feat: implement session lifetimes

This commit is contained in:
Max Peintner
2024-11-27 11:02:34 +01:00
parent dae8b28597
commit 9573cdba04
7 changed files with 93 additions and 64 deletions

View File

@@ -7,7 +7,7 @@ import {
getSession, getSession,
setSession, setSession,
} from "@/lib/zitadel"; } from "@/lib/zitadel";
import { timestampMs } from "@zitadel/client"; import { Duration, timestampMs } from "@zitadel/client";
import { import {
Challenges, Challenges,
RequestChallenges, RequestChallenges,
@@ -30,6 +30,7 @@ export async function createSessionAndUpdateCookie(
checks: Checks, checks: Checks,
challenges: RequestChallenges | undefined, challenges: RequestChallenges | undefined,
authRequestId: string | undefined, authRequestId: string | undefined,
lifetime?: Duration,
): Promise<Session> { ): Promise<Session> {
const createdSession = await createSessionFromChecks(checks, challenges); const createdSession = await createSessionFromChecks(checks, challenges);
@@ -82,10 +83,12 @@ export async function createSessionForIdpAndUpdateCookie(
idpIntentToken?: string | undefined; idpIntentToken?: string | undefined;
}, },
authRequestId: string | undefined, authRequestId: string | undefined,
lifetime?: Duration,
): Promise<Session> { ): Promise<Session> {
const createdSession = await createSessionForUserIdAndIdpIntent( const createdSession = await createSessionForUserIdAndIdpIntent(
userId, userId,
idpIntent, idpIntent,
lifetime,
); );
if (createdSession) { if (createdSession) {
@@ -140,12 +143,14 @@ export async function setSessionAndUpdateCookie(
checks?: Checks, checks?: Checks,
challenges?: RequestChallenges, challenges?: RequestChallenges,
authRequestId?: string, authRequestId?: string,
lifetime?: Duration,
) { ) {
return setSession( return setSession(
recentCookie.id, recentCookie.id,
recentCookie.token, recentCookie.token,
challenges, challenges,
checks, checks,
lifetime,
).then((updatedSession) => { ).then((updatedSession) => {
if (updatedSession) { if (updatedSession) {
const sessionCookie: CustomCookieData = { const sessionCookie: CustomCookieData = {

View File

@@ -12,6 +12,7 @@ import {
getSessionCookieById, getSessionCookieById,
getSessionCookieByLoginName, getSessionCookieByLoginName,
} from "../cookies"; } from "../cookies";
import { getLoginSettings } from "../zitadel";
export type SetOTPCommand = { export type SetOTPCommand = {
loginName?: string; loginName?: string;
@@ -23,49 +24,52 @@ export type SetOTPCommand = {
}; };
export async function setOTP(command: SetOTPCommand) { export async function setOTP(command: SetOTPCommand) {
const recentPromise = command.sessionId const recentSession = command.sessionId
? getSessionCookieById({ sessionId: command.sessionId }).catch((error) => { ? await getSessionCookieById({ sessionId: command.sessionId }).catch(
return Promise.reject(error); (error) => {
}) return Promise.reject(error);
},
)
: command.loginName : command.loginName
? getSessionCookieByLoginName({ ? await getSessionCookieByLoginName({
loginName: command.loginName, loginName: command.loginName,
organization: command.organization, organization: command.organization,
}).catch((error) => { }).catch((error) => {
return Promise.reject(error); return Promise.reject(error);
}) })
: getMostRecentSessionCookie().catch((error) => { : await getMostRecentSessionCookie().catch((error) => {
return Promise.reject(error); return Promise.reject(error);
}); });
return recentPromise.then((recent) => { const checks = create(ChecksSchema, {});
const checks = create(ChecksSchema, {});
if (command.method === "time-based") { if (command.method === "time-based") {
checks.totp = create(CheckTOTPSchema, { checks.totp = create(CheckTOTPSchema, {
code: command.code, code: command.code,
});
} else if (command.method === "sms") {
checks.otpSms = create(CheckOTPSchema, {
code: command.code,
});
} else if (command.method === "email") {
checks.otpEmail = create(CheckOTPSchema, {
code: command.code,
});
}
return setSessionAndUpdateCookie(
recent,
checks,
undefined,
command.authRequestId,
).then((session) => {
return {
sessionId: session.id,
factors: session.factors,
challenges: session.challenges,
};
}); });
} else if (command.method === "sms") {
checks.otpSms = create(CheckOTPSchema, {
code: command.code,
});
} else if (command.method === "email") {
checks.otpEmail = create(CheckOTPSchema, {
code: command.code,
});
}
const loginSettings = await getLoginSettings(command.organization);
return setSessionAndUpdateCookie(
recentSession,
checks,
undefined,
command.authRequestId,
loginSettings?.secondFactorCheckLifetime,
).then((session) => {
return {
sessionId: session.id,
factors: session.factors,
challenges: session.challenges,
};
}); });
} }

View File

@@ -17,6 +17,7 @@ import {
Checks, Checks,
ChecksSchema, ChecksSchema,
} from "@zitadel/proto/zitadel/session/v2/session_service_pb"; } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
import { User, UserState } from "@zitadel/proto/zitadel/user/v2/user_pb"; import { User, UserState } from "@zitadel/proto/zitadel/user/v2/user_pb";
import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
import { headers } from "next/headers"; import { headers } from "next/headers";
@@ -66,6 +67,8 @@ export async function sendPassword(command: UpdateSessionCommand) {
let session; let session;
let user: User; let user: User;
let loginSettings: LoginSettings | undefined;
if (!sessionCookie) { if (!sessionCookie) {
const users = await listUsers({ const users = await listUsers({
loginName: command.loginName, loginName: command.loginName,
@@ -80,10 +83,13 @@ export async function sendPassword(command: UpdateSessionCommand) {
password: { password: command.checks.password?.password }, password: { password: command.checks.password?.password },
}); });
loginSettings = await getLoginSettings(command.organization);
session = await createSessionAndUpdateCookie( session = await createSessionAndUpdateCookie(
checks, checks,
undefined, undefined,
command.authRequestId, command.authRequestId,
loginSettings?.passwordCheckLifetime,
); );
} }
@@ -95,6 +101,7 @@ export async function sendPassword(command: UpdateSessionCommand) {
command.checks, command.checks,
undefined, undefined,
command.authRequestId, command.authRequestId,
loginSettings?.passwordCheckLifetime,
); );
if (!session?.factors?.user?.id) { if (!session?.factors?.user?.id) {
@@ -110,6 +117,12 @@ export async function sendPassword(command: UpdateSessionCommand) {
user = userResponse.user; user = userResponse.user;
} }
if (!loginSettings) {
loginSettings = await getLoginSettings(
command.organization ?? session.factors?.user?.organizationId,
);
}
if (!session?.factors?.user?.id || !sessionCookie) { if (!session?.factors?.user?.id || !sessionCookie) {
return { error: "Could not create session for user" }; return { error: "Could not create session for user" };
} }
@@ -241,9 +254,6 @@ export async function sendPassword(command: UpdateSessionCommand) {
// return router.push(`/passkey/set?` + params); // return router.push(`/passkey/set?` + params);
// } // }
else if (command.authRequestId && session.id) { else if (command.authRequestId && session.id) {
const loginSettings = await getLoginSettings(
command.organization ?? session.factors?.user?.organizationId,
);
const nextUrl = await getNextUrl( const nextUrl = await getNextUrl(
{ {
sessionId: session.id, sessionId: session.id,
@@ -257,9 +267,6 @@ export async function sendPassword(command: UpdateSessionCommand) {
return { redirect: nextUrl }; return { redirect: nextUrl };
} }
const loginSettings = await getLoginSettings(
command.organization ?? session.factors?.user?.organizationId,
);
const url = await getNextUrl( const url = await getNextUrl(
{ {
loginName: session.factors.user.loginName, loginName: session.factors.user.loginName,

View File

@@ -37,6 +37,8 @@ export async function registerUser(command: RegisterUserCommand) {
return { error: "Could not create user" }; return { error: "Could not create user" };
} }
const loginSettings = await getLoginSettings(command.organization);
let checkPayload: any = { let checkPayload: any = {
user: { search: { case: "userId", value: human.userId } }, user: { search: { case: "userId", value: human.userId } },
}; };
@@ -54,6 +56,7 @@ export async function registerUser(command: RegisterUserCommand) {
checks, checks,
undefined, undefined,
command.authRequestId, command.authRequestId,
command.password ? loginSettings?.passwordCheckLifetime : undefined,
); );
if (!session || !session.factors?.user) { if (!session || !session.factors?.user) {
@@ -72,10 +75,6 @@ export async function registerUser(command: RegisterUserCommand) {
return { redirect: "/passkey/set?" + params }; return { redirect: "/passkey/set?" + params };
} else { } else {
const loginSettings = await getLoginSettings(
session.factors.user.organizationId,
);
const url = await getNextUrl( const url = await getNextUrl(
command.authRequestId && session.id command.authRequestId && session.id
? { ? {

View File

@@ -7,8 +7,10 @@ import {
import { import {
deleteSession, deleteSession,
getLoginSettings, getLoginSettings,
getUserByID,
listAuthenticationMethodTypes, listAuthenticationMethodTypes,
} from "@/lib/zitadel"; } from "@/lib/zitadel";
import { Duration } from "@zitadel/client";
import { RequestChallenges } from "@zitadel/proto/zitadel/session/v2/challenge_pb"; import { RequestChallenges } from "@zitadel/proto/zitadel/session/v2/challenge_pb";
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb"; import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
import { Checks } from "@zitadel/proto/zitadel/session/v2/session_service_pb"; import { Checks } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
@@ -38,20 +40,26 @@ export async function createNewSessionForIdp(options: CreateNewSessionCommand) {
if (!userId || !idpIntent) { if (!userId || !idpIntent) {
throw new Error("No userId or loginName provided"); throw new Error("No userId or loginName provided");
} }
const user = await getUserByID(userId);
if (!user) {
return { error: "Could not find user" };
}
const loginSettings = await getLoginSettings(user.details?.resourceOwner);
const session = await createSessionForIdpAndUpdateCookie( const session = await createSessionForIdpAndUpdateCookie(
userId, userId,
idpIntent, idpIntent,
authRequestId, authRequestId,
loginSettings?.externalLoginCheckLifetime,
); );
if (!session || !session.factors?.user) { if (!session || !session.factors?.user) {
return { error: "Could not create session" }; return { error: "Could not create session" };
} }
const loginSettings = await getLoginSettings(
session.factors.user.organizationId,
);
const url = await getNextUrl( const url = await getNextUrl(
authRequestId && session.id authRequestId && session.id
? { ? {
@@ -110,6 +118,7 @@ export type UpdateSessionCommand = {
checks?: Checks; checks?: Checks;
authRequestId?: string; authRequestId?: string;
challenges?: RequestChallenges; challenges?: RequestChallenges;
lifetime?: Duration;
}; };
export async function updateSession(options: UpdateSessionCommand) { export async function updateSession(options: UpdateSessionCommand) {
@@ -121,17 +130,17 @@ export async function updateSession(options: UpdateSessionCommand) {
authRequestId, authRequestId,
challenges, challenges,
} = options; } = options;
const sessionPromise = sessionId const recentSession = sessionId
? getSessionCookieById({ sessionId }).catch((error) => { ? await getSessionCookieById({ sessionId }).catch((error) => {
return Promise.reject(error); return Promise.reject(error);
}) })
: loginName : loginName
? getSessionCookieByLoginName({ loginName, organization }).catch( ? await getSessionCookieByLoginName({ loginName, organization }).catch(
(error) => { (error) => {
return Promise.reject(error); return Promise.reject(error);
}, },
) )
: getMostRecentSessionCookie().catch((error) => { : await getMostRecentSessionCookie().catch((error) => {
return Promise.reject(error); return Promise.reject(error);
}); });
@@ -148,13 +157,20 @@ export async function updateSession(options: UpdateSessionCommand) {
challenges.webAuthN.domain = hostname; challenges.webAuthN.domain = hostname;
} }
const recent = await sessionPromise; const loginSettings = await getLoginSettings(organization);
const lifetime = checks?.webAuthN
? loginSettings?.multiFactorCheckLifetime // TODO different lifetime for webauthn u2f/passkey
: checks?.otpEmail || checks?.otpSms
? loginSettings?.secondFactorCheckLifetime
: undefined;
const session = await setSessionAndUpdateCookie( const session = await setSessionAndUpdateCookie(
recent, recentSession,
checks, checks,
challenges, challenges,
authRequestId, authRequestId,
lifetime,
); );
// if password, check if user has MFA methods // if password, check if user has MFA methods

View File

@@ -18,7 +18,7 @@ import {
VerifyU2FRegistrationRequest, VerifyU2FRegistrationRequest,
} from "@zitadel/proto/zitadel/user/v2/user_service_pb"; } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
import { create } from "@zitadel/client"; import { create, Duration } from "@zitadel/client";
import { TextQueryMethod } from "@zitadel/proto/zitadel/object/v2/object_pb"; import { TextQueryMethod } from "@zitadel/proto/zitadel/object/v2/object_pb";
import { CreateCallbackRequest } from "@zitadel/proto/zitadel/oidc/v2/oidc_service_pb"; import { CreateCallbackRequest } from "@zitadel/proto/zitadel/oidc/v2/oidc_service_pb";
import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb"; import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb";
@@ -132,15 +132,13 @@ export async function getPasswordComplexitySettings(organization?: string) {
export async function createSessionFromChecks( export async function createSessionFromChecks(
checks: Checks, checks: Checks,
challenges: RequestChallenges | undefined, challenges: RequestChallenges | undefined,
lifetime?: Duration,
) { ) {
return sessionService.createSession( return sessionService.createSession(
{ {
checks: checks, checks: checks,
challenges, challenges,
lifetime: { lifetime,
seconds: BigInt(SESSION_LIFETIME_S),
nanos: 0,
},
}, },
{}, {},
); );
@@ -152,6 +150,7 @@ export async function createSessionForUserIdAndIdpIntent(
idpIntentId?: string | undefined; idpIntentId?: string | undefined;
idpIntentToken?: string | undefined; idpIntentToken?: string | undefined;
}, },
lifetime?: Duration,
) { ) {
return sessionService.createSession({ return sessionService.createSession({
checks: { checks: {
@@ -163,10 +162,7 @@ export async function createSessionForUserIdAndIdpIntent(
}, },
idpIntent, idpIntent,
}, },
// lifetime: { lifetime,
// seconds: 300,
// nanos: 0,
// },
}); });
} }
@@ -175,6 +171,7 @@ export async function setSession(
sessionToken: string, sessionToken: string,
challenges: RequestChallenges | undefined, challenges: RequestChallenges | undefined,
checks?: Checks, checks?: Checks,
lifetime?: Duration,
) { ) {
return sessionService.setSession( return sessionService.setSession(
{ {
@@ -183,6 +180,7 @@ export async function setSession(
challenges, challenges,
checks: checks ? checks : {}, checks: checks ? checks : {},
metadata: {}, metadata: {},
lifetime,
}, },
{}, {},
); );

View File

@@ -4,4 +4,4 @@ export { NewAuthorizationBearerInterceptor } from "./interceptors";
// TODO: Move this to `./protobuf.ts` and export it from there // TODO: Move this to `./protobuf.ts` and export it from there
export { create, fromJson, toJson } from "@bufbuild/protobuf"; export { create, fromJson, toJson } from "@bufbuild/protobuf";
export { TimestampSchema, timestampDate, timestampFromDate, timestampFromMs, timestampMs } from "@bufbuild/protobuf/wkt"; export { TimestampSchema, timestampDate, timestampFromDate, timestampFromMs, timestampMs } from "@bufbuild/protobuf/wkt";
export type { Timestamp } from "@bufbuild/protobuf/wkt"; export type { Duration, Timestamp } from "@bufbuild/protobuf/wkt";