mirror of
https://github.com/zitadel/zitadel.git
synced 2025-12-12 01:34:04 +00:00
feat: implement session lifetimes
This commit is contained in:
@@ -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 = {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
? {
|
? {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
{},
|
{},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
Reference in New Issue
Block a user