mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
feat(user-auth): improve token refresh logic and default values
This commit is contained in:
@@ -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<void> {
|
||||
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 });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<typeof OrganizationsSchema>;
|
||||
|
||||
@@ -6,4 +6,5 @@ export * from "./array";
|
||||
export * from "./dates";
|
||||
export * from "./object";
|
||||
export * from "./string";
|
||||
export * from "./time";
|
||||
export * from "./undefined";
|
||||
|
||||
34
backend/src/lib/fn/time.ts
Normal file
34
backend/src/lib/fn/time.ts
Normal file
@@ -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;
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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<TProjectDALFactory, "findProjectGhostUser" | "findProjectById">;
|
||||
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
|
||||
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
|
||||
orgService: Pick<TOrgServiceFactory, "createOrganization">;
|
||||
orgService: Pick<TOrgServiceFactory, "createOrganization" | "findOrganizationById">;
|
||||
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 };
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
/*
|
||||
|
||||
Reference in New Issue
Block a user