From e99e3603395aaec37a44e757b6cd33ae1acf596c Mon Sep 17 00:00:00 2001
From: carlosmonastyrski
Date: Mon, 28 Apr 2025 17:43:10 -0300
Subject: [PATCH 1/5] feat(user-auth): make users auth token expiration
customizable for orgs
---
...6_add-org-user-token-expiration-setting.ts | 19 +++
backend/src/db/schemas/organizations.ts | 3 +-
backend/src/server/routes/v1/auth-router.ts | 14 +-
.../server/routes/v1/organization-router.ts | 14 +-
backend/src/services/org/org-schema.ts | 3 +-
backend/src/services/org/org-service.ts | 6 +-
backend/src/services/org/org-types.ts | 1 +
.../src/hooks/api/organization/queries.tsx | 6 +-
frontend/src/hooks/api/organization/types.ts | 2 +
.../components/OrgAuthTab/OrgAuthTab.tsx | 2 +
.../OrgUserAccessTokenLimitSection.tsx | 158 ++++++++++++++++++
11 files changed, 220 insertions(+), 8 deletions(-)
create mode 100644 backend/src/db/migrations/20250428134716_add-org-user-token-expiration-setting.ts
create mode 100644 frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/OrgUserAccessTokenLimitSection.tsx
diff --git a/backend/src/db/migrations/20250428134716_add-org-user-token-expiration-setting.ts b/backend/src/db/migrations/20250428134716_add-org-user-token-expiration-setting.ts
new file mode 100644
index 000000000..96994273a
--- /dev/null
+++ b/backend/src/db/migrations/20250428134716_add-org-user-token-expiration-setting.ts
@@ -0,0 +1,19 @@
+import { Knex } from "knex";
+
+import { TableName } from "../schemas";
+
+export async function up(knex: Knex): Promise {
+ if (!(await knex.schema.hasColumn(TableName.Organization, "userTokenExpiration"))) {
+ await knex.schema.alterTable(TableName.Organization, (t) => {
+ t.string("userTokenExpiration").defaultTo("30d").notNullable();
+ });
+ }
+}
+
+export async function down(knex: Knex): Promise {
+ if (await knex.schema.hasColumn(TableName.Organization, "userTokenExpiration")) {
+ await knex.schema.alterTable(TableName.Organization, (t) => {
+ t.dropColumn("userTokenExpiration");
+ });
+ }
+}
diff --git a/backend/src/db/schemas/organizations.ts b/backend/src/db/schemas/organizations.ts
index 902c564a7..b4331c5b1 100644
--- a/backend/src/db/schemas/organizations.ts
+++ b/backend/src/db/schemas/organizations.ts
@@ -28,7 +28,8 @@ export const OrganizationsSchema = z.object({
shouldUseNewPrivilegeSystem: z.boolean().default(true),
privilegeUpgradeInitiatedByUsername: z.string().nullable().optional(),
privilegeUpgradeInitiatedAt: z.date().nullable().optional(),
- bypassOrgAuthEnabled: z.boolean().default(false)
+ bypassOrgAuthEnabled: z.boolean().default(false),
+ userTokenExpiration: z.string().default("30d")
});
export type TOrganizations = z.infer;
diff --git a/backend/src/server/routes/v1/auth-router.ts b/backend/src/server/routes/v1/auth-router.ts
index 717c6f1b6..38934e1eb 100644
--- a/backend/src/server/routes/v1/auth-router.ts
+++ b/backend/src/server/routes/v1/auth-router.ts
@@ -79,6 +79,18 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => {
handler: async (req) => {
const { decodedToken, tokenVersion } = await server.services.authToken.validateRefreshToken(req.cookies.jid);
const appCfg = getConfig();
+ let expiresIn = appCfg.JWT_AUTH_LIFETIME;
+ if (decodedToken.organizationId) {
+ const org = await server.services.org.findOrganizationById(
+ decodedToken.userId,
+ decodedToken.organizationId,
+ decodedToken.authMethod,
+ decodedToken.organizationId
+ );
+ if (org) {
+ expiresIn = org.userTokenExpiration;
+ }
+ }
const token = jwt.sign(
{
@@ -92,7 +104,7 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => {
mfaMethod: decodedToken.mfaMethod
},
appCfg.AUTH_SECRET,
- { expiresIn: appCfg.JWT_AUTH_LIFETIME }
+ { expiresIn }
);
return { token, organizationId: decodedToken.organizationId };
diff --git a/backend/src/server/routes/v1/organization-router.ts b/backend/src/server/routes/v1/organization-router.ts
index 22314b54b..7cde626bc 100644
--- a/backend/src/server/routes/v1/organization-router.ts
+++ b/backend/src/server/routes/v1/organization-router.ts
@@ -1,3 +1,4 @@
+import RE2 from "re2";
import { z } from "zod";
import {
@@ -263,7 +264,18 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
enforceMfa: z.boolean().optional(),
selectedMfaMethod: z.nativeEnum(MfaMethod).optional(),
allowSecretSharingOutsideOrganization: z.boolean().optional(),
- bypassOrgAuthEnabled: z.boolean().optional()
+ bypassOrgAuthEnabled: z.boolean().optional(),
+ userTokenExpiration: z
+ .string()
+ .regex(new RE2(/^\d+[mhdw]$/), "Must be a number followed by m, h, d, or w")
+ .refine(
+ (val) => {
+ const numericPart = val.slice(0, -1);
+ return parseInt(numericPart, 10) >= 1;
+ },
+ { message: "Duration value must be at least 1" }
+ )
+ .optional()
}),
response: {
200: z.object({
diff --git a/backend/src/services/org/org-schema.ts b/backend/src/services/org/org-schema.ts
index 2aa793c04..5a1a4c333 100644
--- a/backend/src/services/org/org-schema.ts
+++ b/backend/src/services/org/org-schema.ts
@@ -17,5 +17,6 @@ export const sanitizedOrganizationSchema = OrganizationsSchema.pick({
shouldUseNewPrivilegeSystem: true,
privilegeUpgradeInitiatedByUsername: true,
privilegeUpgradeInitiatedAt: true,
- bypassOrgAuthEnabled: true
+ bypassOrgAuthEnabled: true,
+ userTokenExpiration: true
});
diff --git a/backend/src/services/org/org-service.ts b/backend/src/services/org/org-service.ts
index 3a6373575..4b7192b99 100644
--- a/backend/src/services/org/org-service.ts
+++ b/backend/src/services/org/org-service.ts
@@ -350,7 +350,8 @@ export const orgServiceFactory = ({
enforceMfa,
selectedMfaMethod,
allowSecretSharingOutsideOrganization,
- bypassOrgAuthEnabled
+ bypassOrgAuthEnabled,
+ userTokenExpiration
}
}: TUpdateOrgDTO) => {
const appCfg = getConfig();
@@ -451,7 +452,8 @@ export const orgServiceFactory = ({
enforceMfa,
selectedMfaMethod,
allowSecretSharingOutsideOrganization,
- bypassOrgAuthEnabled
+ bypassOrgAuthEnabled,
+ userTokenExpiration
});
if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` });
return org;
diff --git a/backend/src/services/org/org-types.ts b/backend/src/services/org/org-types.ts
index 8a1698015..702cd25bf 100644
--- a/backend/src/services/org/org-types.ts
+++ b/backend/src/services/org/org-types.ts
@@ -74,6 +74,7 @@ export type TUpdateOrgDTO = {
selectedMfaMethod: MfaMethod;
allowSecretSharingOutsideOrganization: boolean;
bypassOrgAuthEnabled: boolean;
+ userTokenExpiration: string;
}>;
} & TOrgPermission;
diff --git a/frontend/src/hooks/api/organization/queries.tsx b/frontend/src/hooks/api/organization/queries.tsx
index 902bb6b09..06125b1d7 100644
--- a/frontend/src/hooks/api/organization/queries.tsx
+++ b/frontend/src/hooks/api/organization/queries.tsx
@@ -111,7 +111,8 @@ export const useUpdateOrg = () => {
enforceMfa,
selectedMfaMethod,
allowSecretSharingOutsideOrganization,
- bypassOrgAuthEnabled
+ bypassOrgAuthEnabled,
+ userTokenExpiration
}) => {
return apiRequest.patch(`/api/v1/organization/${orgId}`, {
name,
@@ -122,7 +123,8 @@ export const useUpdateOrg = () => {
enforceMfa,
selectedMfaMethod,
allowSecretSharingOutsideOrganization,
- bypassOrgAuthEnabled
+ bypassOrgAuthEnabled,
+ userTokenExpiration
});
},
onSuccess: () => {
diff --git a/frontend/src/hooks/api/organization/types.ts b/frontend/src/hooks/api/organization/types.ts
index e0687922d..6f63d003e 100644
--- a/frontend/src/hooks/api/organization/types.ts
+++ b/frontend/src/hooks/api/organization/types.ts
@@ -18,6 +18,7 @@ export type Organization = {
selectedMfaMethod?: MfaMethod;
shouldUseNewPrivilegeSystem: boolean;
allowSecretSharingOutsideOrganization?: boolean;
+ userTokenExpiration?: string;
userRole: string;
};
@@ -32,6 +33,7 @@ export type UpdateOrgDTO = {
selectedMfaMethod?: MfaMethod;
allowSecretSharingOutsideOrganization?: boolean;
bypassOrgAuthEnabled?: boolean;
+ userTokenExpiration?: string;
};
export type BillingDetails = {
diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/OrgAuthTab.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/OrgAuthTab.tsx
index 40d0e7840..9e542d821 100644
--- a/frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/OrgAuthTab.tsx
+++ b/frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/OrgAuthTab.tsx
@@ -22,6 +22,7 @@ import { OrgLDAPSection } from "./OrgLDAPSection";
import { OrgOIDCSection } from "./OrgOIDCSection";
import { OrgScimSection } from "./OrgSCIMSection";
import { OrgSSOSection } from "./OrgSSOSection";
+import { OrgUserAccessTokenLimitSection } from "./OrgUserAccessTokenLimitSection";
import { SSOModal } from "./SSOModal";
export const OrgAuthTab = withPermission(
@@ -166,6 +167,7 @@ export const OrgAuthTab = withPermission(
return (
<>
+
{shouldShowCreateIdentityProviderView ? (
createIdentityProviderView
) : (
diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/OrgUserAccessTokenLimitSection.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/OrgUserAccessTokenLimitSection.tsx
new file mode 100644
index 000000000..a4d074819
--- /dev/null
+++ b/frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/OrgUserAccessTokenLimitSection.tsx
@@ -0,0 +1,158 @@
+import { Controller, useForm } from "react-hook-form";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { z } from "zod";
+
+import { createNotification } from "@app/components/notifications";
+import { Button, FormControl, Input, Select, SelectItem } from "@app/components/v2";
+import { useOrganization } from "@app/context";
+import { useUpdateOrg } from "@app/hooks/api";
+
+const formSchema = z.object({
+ expirationValue: z.number().min(1, "Value must be at least 1"),
+ expirationUnit: z.enum(["m", "h", "d", "w"], {
+ invalid_type_error: "Please select a valid time unit"
+ })
+});
+
+type TForm = z.infer;
+
+// Function to parse duration string like "30d" into value and unit
+const parseDuration = (duration: string): { value: number; unit: string } => {
+ const match = duration.match(/^(\d+)([mhdw])$/);
+ if (match) {
+ return {
+ value: parseInt(match[1], 10),
+ unit: match[2]
+ };
+ }
+ // Default to 30 days if invalid format
+ return { value: 30, unit: "d" };
+};
+
+// Function to format value and unit back to duration string
+const formatDuration = (value: number, unit: string): string => {
+ return `${value}${unit}`;
+};
+
+export const OrgUserAccessTokenLimitSection = () => {
+ const { mutateAsync: updateUserTokenExpiration } = useUpdateOrg();
+ const { currentOrg } = useOrganization();
+
+ // Parse the current duration or use default
+ const currentDuration = parseDuration(currentOrg?.userTokenExpiration || "30d");
+
+ const {
+ control,
+ formState: { isSubmitting, isDirty },
+ handleSubmit
+ } = useForm({
+ resolver: zodResolver(formSchema),
+ defaultValues: {
+ expirationValue: currentDuration.value,
+ expirationUnit: currentDuration.unit as "m" | "h" | "d" | "w"
+ }
+ });
+
+ if (!currentOrg) return null;
+
+ const handleUserTokenExpirationSubmit = async (formData: TForm) => {
+ try {
+ const userTokenExpiration = formatDuration(formData.expirationValue, formData.expirationUnit);
+
+ await updateUserTokenExpiration({
+ userTokenExpiration,
+ orgId: currentOrg.id
+ });
+
+ createNotification({
+ text: "Successfully updated user token expiration",
+ type: "success"
+ });
+ } catch {
+ createNotification({
+ text: "Failed updating user token expiration",
+ type: "error"
+ });
+ }
+ };
+
+ // Units for the dropdown with readable labels
+ const timeUnits = [
+ { value: "m", label: "Minutes" },
+ { value: "h", label: "Hours" },
+ { value: "d", label: "Days" },
+ { value: "w", label: "Weeks" }
+ ];
+
+ return (
+
+
+
User Token Expiration
+
+
+ This defines the maximum time a user token will be valid. After this time, the user will
+ need to re-authenticate.
+
+
+
+ );
+};
From 024ed0c0d857b23d4dd2b22c37fff11eaa468068 Mon Sep 17 00:00:00 2001
From: carlosmonastyrski
Date: Mon, 28 Apr 2025 18:19:44 -0300
Subject: [PATCH 2/5] feat(user-auth): add pr suggestions
---
.../OrgUserAccessTokenLimitSection.tsx | 133 ++++++++++--------
1 file changed, 73 insertions(+), 60 deletions(-)
diff --git a/frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/OrgUserAccessTokenLimitSection.tsx b/frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/OrgUserAccessTokenLimitSection.tsx
index a4d074819..43e72bd41 100644
--- a/frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/OrgUserAccessTokenLimitSection.tsx
+++ b/frontend/src/pages/organization/SettingsPage/components/OrgAuthTab/OrgUserAccessTokenLimitSection.tsx
@@ -3,8 +3,9 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
+import { OrgPermissionCan } from "@app/components/permissions";
import { Button, FormControl, Input, Select, SelectItem } from "@app/components/v2";
-import { useOrganization } from "@app/context";
+import { OrgPermissionActions, OrgPermissionSubjects, useOrganization } from "@app/context";
import { useUpdateOrg } from "@app/hooks/api";
const formSchema = z.object({
@@ -93,66 +94,78 @@ export const OrgUserAccessTokenLimitSection = () => {
This defines the maximum time a user token will be valid. After this time, the user will
need to re-authenticate.
-
+ )}
+
);
};
From 7bd61d88fc0b29ceb1e382f2b0b73dc0bf1f93a7 Mon Sep 17 00:00:00 2001
From: carlosmonastyrski
Date: Tue, 29 Apr 2025 18:28:18 -0300
Subject: [PATCH 3/5] feat(user-auth): improve token refresh logic and default
values
---
...6_add-org-user-token-expiration-setting.ts | 6 +++-
backend/src/db/schemas/organizations.ts | 2 +-
backend/src/lib/fn/index.ts | 1 +
backend/src/lib/fn/time.ts | 34 +++++++++++++++++++
backend/src/server/routes/v1/auth-router.ts | 7 ++--
.../server/routes/v1/organization-router.ts | 2 +-
.../src/services/auth/auth-login-service.ts | 17 ++++++++--
.../src/services/auth/auth-signup-service.ts | 18 ++++++++--
backend/src/services/org/org-service.ts | 4 +++
9 files changed, 79 insertions(+), 12 deletions(-)
create mode 100644 backend/src/lib/fn/time.ts
diff --git a/backend/src/db/migrations/20250428134716_add-org-user-token-expiration-setting.ts b/backend/src/db/migrations/20250428134716_add-org-user-token-expiration-setting.ts
index 96994273a..9e06f18ef 100644
--- a/backend/src/db/migrations/20250428134716_add-org-user-token-expiration-setting.ts
+++ b/backend/src/db/migrations/20250428134716_add-org-user-token-expiration-setting.ts
@@ -1,12 +1,16 @@
import { Knex } from "knex";
+import { getConfig } from "@app/lib/config/env";
+
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise {
+ const appCfg = getConfig();
if (!(await knex.schema.hasColumn(TableName.Organization, "userTokenExpiration"))) {
await knex.schema.alterTable(TableName.Organization, (t) => {
- t.string("userTokenExpiration").defaultTo("30d").notNullable();
+ t.string("userTokenExpiration");
});
+ await knex(TableName.Organization).update({ userTokenExpiration: appCfg.JWT_REFRESH_LIFETIME });
}
}
diff --git a/backend/src/db/schemas/organizations.ts b/backend/src/db/schemas/organizations.ts
index b4331c5b1..bc6f0b7af 100644
--- a/backend/src/db/schemas/organizations.ts
+++ b/backend/src/db/schemas/organizations.ts
@@ -29,7 +29,7 @@ export const OrganizationsSchema = z.object({
privilegeUpgradeInitiatedByUsername: z.string().nullable().optional(),
privilegeUpgradeInitiatedAt: z.date().nullable().optional(),
bypassOrgAuthEnabled: z.boolean().default(false),
- userTokenExpiration: z.string().default("30d")
+ userTokenExpiration: z.string().nullable().optional()
});
export type TOrganizations = z.infer;
diff --git a/backend/src/lib/fn/index.ts b/backend/src/lib/fn/index.ts
index 82a4c4914..ae704cf9f 100644
--- a/backend/src/lib/fn/index.ts
+++ b/backend/src/lib/fn/index.ts
@@ -6,4 +6,5 @@ export * from "./array";
export * from "./dates";
export * from "./object";
export * from "./string";
+export * from "./time";
export * from "./undefined";
diff --git a/backend/src/lib/fn/time.ts b/backend/src/lib/fn/time.ts
new file mode 100644
index 000000000..a7fca66e8
--- /dev/null
+++ b/backend/src/lib/fn/time.ts
@@ -0,0 +1,34 @@
+const convertToMilliseconds = (exp: string | number): number => {
+ if (typeof exp === "number") {
+ return exp * 1000;
+ }
+
+ const match = exp.match(/^(\d+)\s*([a-z]*)$/i);
+ if (!match) {
+ throw new Error(`Invalid expiration format: ${exp}`);
+ }
+
+ const value = parseInt(match[1], 10);
+ const unit = match[2].toLowerCase();
+
+ switch (unit) {
+ case "":
+ case "s":
+ return value * 1000; // seconds
+ case "m":
+ return value * 60 * 1000; // minutes
+ case "h":
+ return value * 60 * 60 * 1000; // hours
+ case "d":
+ return value * 24 * 60 * 60 * 1000; // days
+ default:
+ throw new Error(`Unsupported time unit: ${unit}`);
+ }
+};
+
+export const getMinExpiresIn = (exp1: string | number, exp2: string | number): string | number => {
+ const ms1 = convertToMilliseconds(exp1);
+ const ms2 = convertToMilliseconds(exp2);
+
+ return ms1 <= ms2 ? exp1 : exp2;
+};
diff --git a/backend/src/server/routes/v1/auth-router.ts b/backend/src/server/routes/v1/auth-router.ts
index 38934e1eb..7231ce85c 100644
--- a/backend/src/server/routes/v1/auth-router.ts
+++ b/backend/src/server/routes/v1/auth-router.ts
@@ -2,6 +2,7 @@ import jwt from "jsonwebtoken";
import { z } from "zod";
import { getConfig } from "@app/lib/config/env";
+import { getMinExpiresIn } from "@app/lib/fn";
import { authRateLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode, AuthTokenType } from "@app/services/auth/auth-type";
@@ -79,7 +80,7 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => {
handler: async (req) => {
const { decodedToken, tokenVersion } = await server.services.authToken.validateRefreshToken(req.cookies.jid);
const appCfg = getConfig();
- let expiresIn = appCfg.JWT_AUTH_LIFETIME;
+ let expiresIn: string | number = appCfg.JWT_AUTH_LIFETIME;
if (decodedToken.organizationId) {
const org = await server.services.org.findOrganizationById(
decodedToken.userId,
@@ -87,8 +88,8 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => {
decodedToken.authMethod,
decodedToken.organizationId
);
- if (org) {
- expiresIn = org.userTokenExpiration;
+ if (org && org.userTokenExpiration) {
+ expiresIn = getMinExpiresIn(appCfg.JWT_AUTH_LIFETIME, org.userTokenExpiration);
}
}
diff --git a/backend/src/server/routes/v1/organization-router.ts b/backend/src/server/routes/v1/organization-router.ts
index 7cde626bc..da1a251ff 100644
--- a/backend/src/server/routes/v1/organization-router.ts
+++ b/backend/src/server/routes/v1/organization-router.ts
@@ -267,7 +267,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
bypassOrgAuthEnabled: z.boolean().optional(),
userTokenExpiration: z
.string()
- .regex(new RE2(/^\d+[mhdw]$/), "Must be a number followed by m, h, d, or w")
+ .refine((val) => new RE2(/^\d+[mhdw]$/).test(val), "Must be a number followed by m, h, d, or w")
.refine(
(val) => {
const numericPart = val.slice(0, -1);
diff --git a/backend/src/services/auth/auth-login-service.ts b/backend/src/services/auth/auth-login-service.ts
index bc9c4afa3..d5b1b264b 100644
--- a/backend/src/services/auth/auth-login-service.ts
+++ b/backend/src/services/auth/auth-login-service.ts
@@ -12,7 +12,7 @@ import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto";
import { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { getUserPrivateKey } from "@app/lib/crypto/srp";
import { BadRequestError, DatabaseError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors";
-import { removeTrailingSlash } from "@app/lib/fn";
+import { getMinExpiresIn, removeTrailingSlash } from "@app/lib/fn";
import { logger } from "@app/lib/logger";
import { getUserAgentType } from "@app/server/plugins/audit-log";
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
@@ -143,6 +143,17 @@ export const authLoginServiceFactory = ({
);
if (!tokenSession) throw new Error("Failed to create token");
+ let tokenSessionExpiresIn: string | number = cfg.JWT_AUTH_LIFETIME;
+ let refreshTokenExpiresIn: string | number = cfg.JWT_REFRESH_LIFETIME;
+
+ if (organizationId) {
+ const org = await orgDAL.findById(organizationId);
+ if (org && org.userTokenExpiration) {
+ tokenSessionExpiresIn = getMinExpiresIn(cfg.JWT_AUTH_LIFETIME, org.userTokenExpiration);
+ refreshTokenExpiresIn = org.userTokenExpiration;
+ }
+ }
+
const accessToken = jwt.sign(
{
authMethod,
@@ -155,7 +166,7 @@ export const authLoginServiceFactory = ({
mfaMethod
},
cfg.AUTH_SECRET,
- { expiresIn: cfg.JWT_AUTH_LIFETIME }
+ { expiresIn: tokenSessionExpiresIn }
);
const refreshToken = jwt.sign(
@@ -170,7 +181,7 @@ export const authLoginServiceFactory = ({
mfaMethod
},
cfg.AUTH_SECRET,
- { expiresIn: cfg.JWT_REFRESH_LIFETIME }
+ { expiresIn: refreshTokenExpiresIn }
);
return { access: accessToken, refresh: refreshToken };
diff --git a/backend/src/services/auth/auth-signup-service.ts b/backend/src/services/auth/auth-signup-service.ts
index a652c2a5b..58ba9186e 100644
--- a/backend/src/services/auth/auth-signup-service.ts
+++ b/backend/src/services/auth/auth-signup-service.ts
@@ -10,6 +10,7 @@ import { getConfig } from "@app/lib/config/env";
import { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
import { generateUserSrpKeys, getUserPrivateKey } from "@app/lib/crypto/srp";
import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
+import { getMinExpiresIn } from "@app/lib/fn";
import { isDisposableEmail } from "@app/lib/validator";
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
import { TProjectDALFactory } from "@app/services/project/project-dal";
@@ -46,7 +47,7 @@ type TAuthSignupDep = {
projectDAL: Pick;
projectBotDAL: Pick;
groupProjectDAL: Pick;
- orgService: Pick;
+ orgService: Pick;
orgDAL: TOrgDALFactory;
tokenService: TAuthTokenServiceFactory;
smtpService: TSmtpService;
@@ -320,6 +321,17 @@ export const authSignupServiceFactory = ({
projectBotDAL
});
+ let tokenSessionExpiresIn: string | number = appCfg.JWT_AUTH_LIFETIME;
+ let refreshTokenExpiresIn: string | number = appCfg.JWT_REFRESH_LIFETIME;
+
+ if (organizationId) {
+ const org = await orgService.findOrganizationById(user.id, organizationId, authMethod, organizationId);
+ if (org && org.userTokenExpiration) {
+ tokenSessionExpiresIn = getMinExpiresIn(appCfg.JWT_AUTH_LIFETIME, org.userTokenExpiration);
+ refreshTokenExpiresIn = org.userTokenExpiration;
+ }
+ }
+
const tokenSession = await tokenService.getUserTokenSession({
userAgent,
ip,
@@ -337,7 +349,7 @@ export const authSignupServiceFactory = ({
organizationId
},
appCfg.AUTH_SECRET,
- { expiresIn: appCfg.JWT_AUTH_LIFETIME }
+ { expiresIn: tokenSessionExpiresIn }
);
const refreshToken = jwt.sign(
@@ -350,7 +362,7 @@ export const authSignupServiceFactory = ({
organizationId
},
appCfg.AUTH_SECRET,
- { expiresIn: appCfg.JWT_REFRESH_LIFETIME }
+ { expiresIn: refreshTokenExpiresIn }
);
return { user: updateduser.info, accessToken, refreshToken, organizationId };
diff --git a/backend/src/services/org/org-service.ts b/backend/src/services/org/org-service.ts
index 4b7192b99..060a01634 100644
--- a/backend/src/services/org/org-service.ts
+++ b/backend/src/services/org/org-service.ts
@@ -170,8 +170,12 @@ export const orgServiceFactory = ({
actorOrgId: string | undefined
) => {
await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId);
+ const appCfg = getConfig();
const org = await orgDAL.findOrgById(orgId);
if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` });
+ if (!org.userTokenExpiration) {
+ return { ...org, userTokenExpiration: appCfg.JWT_REFRESH_LIFETIME };
+ }
return org;
};
/*
From f0229c5ecf90e8931e69c3da8ae10f85241422a9 Mon Sep 17 00:00:00 2001
From: carlosmonastyrski
Date: Tue, 29 Apr 2025 18:48:08 -0300
Subject: [PATCH 4/5] feat(user-auth): fix migration bug for e2e suite
---
.../20250428134716_add-org-user-token-expiration-setting.ts | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/backend/src/db/migrations/20250428134716_add-org-user-token-expiration-setting.ts b/backend/src/db/migrations/20250428134716_add-org-user-token-expiration-setting.ts
index 9e06f18ef..3f24e4f2c 100644
--- a/backend/src/db/migrations/20250428134716_add-org-user-token-expiration-setting.ts
+++ b/backend/src/db/migrations/20250428134716_add-org-user-token-expiration-setting.ts
@@ -6,11 +6,15 @@ import { TableName } from "../schemas";
export async function up(knex: Knex): Promise {
const appCfg = getConfig();
+ const tokenDuration = appCfg?.JWT_REFRESH_LIFETIME;
+
if (!(await knex.schema.hasColumn(TableName.Organization, "userTokenExpiration"))) {
await knex.schema.alterTable(TableName.Organization, (t) => {
t.string("userTokenExpiration");
});
- await knex(TableName.Organization).update({ userTokenExpiration: appCfg.JWT_REFRESH_LIFETIME });
+ if (tokenDuration) {
+ await knex(TableName.Organization).update({ userTokenExpiration: tokenDuration });
+ }
}
}
From c99440ba81f4d6c21ae8b46f074a3b95b78952d4 Mon Sep 17 00:00:00 2001
From: carlosmonastyrski
Date: Wed, 30 Apr 2025 16:49:33 -0300
Subject: [PATCH 5/5] feat(user-auth): use ms library and update docs
---
backend/src/lib/fn/time.ts | 23 ++++--------------
docs/documentation/platform/organization.mdx | 4 +++
.../organization-settings-auth.png | Bin 531798 -> 360811 bytes
3 files changed, 9 insertions(+), 18 deletions(-)
diff --git a/backend/src/lib/fn/time.ts b/backend/src/lib/fn/time.ts
index a7fca66e8..27bd8f8a6 100644
--- a/backend/src/lib/fn/time.ts
+++ b/backend/src/lib/fn/time.ts
@@ -1,29 +1,16 @@
+import ms, { StringValue } from "ms";
+
const convertToMilliseconds = (exp: string | number): number => {
if (typeof exp === "number") {
return exp * 1000;
}
- const match = exp.match(/^(\d+)\s*([a-z]*)$/i);
- if (!match) {
+ const result = ms(exp as StringValue);
+ if (typeof result !== "number") {
throw new Error(`Invalid expiration format: ${exp}`);
}
- const value = parseInt(match[1], 10);
- const unit = match[2].toLowerCase();
-
- switch (unit) {
- case "":
- case "s":
- return value * 1000; // seconds
- case "m":
- return value * 60 * 1000; // minutes
- case "h":
- return value * 60 * 60 * 1000; // hours
- case "d":
- return value * 24 * 60 * 60 * 1000; // days
- default:
- throw new Error(`Unsupported time unit: ${unit}`);
- }
+ return result;
};
export const getMinExpiresIn = (exp1: string | number, exp2: string | number): string | number => {
diff --git a/docs/documentation/platform/organization.mdx b/docs/documentation/platform/organization.mdx
index f1c62ff37..3a53484fb 100644
--- a/docs/documentation/platform/organization.mdx
+++ b/docs/documentation/platform/organization.mdx
@@ -27,6 +27,10 @@ The **Settings** page lets you manage information about your organization includ

+
+ You can adjust the maximum time a user token will remain valid for your organization. After this period, users will be required to re-authenticate. This helps improve security by enforcing regular sign-ins.
+
+
## Access Control
The **Access Control** page is where you can manage identities (both people and machines) that are part of your organization.
diff --git a/docs/images/platform/organization/organization-settings-auth.png b/docs/images/platform/organization/organization-settings-auth.png
index ca2340e9f2bbfb2418ed38468b43de1520d03e90..fd7a946e021bd8848ad4c6c241ac93ce86478616 100644
GIT binary patch
literal 360811
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