mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-03 13:26:02 +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 { Knex } from "knex";
|
||||||
|
|
||||||
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
|
||||||
import { TableName } from "../schemas";
|
import { TableName } from "../schemas";
|
||||||
|
|
||||||
export async function up(knex: Knex): Promise<void> {
|
export async function up(knex: Knex): Promise<void> {
|
||||||
|
const appCfg = getConfig();
|
||||||
if (!(await knex.schema.hasColumn(TableName.Organization, "userTokenExpiration"))) {
|
if (!(await knex.schema.hasColumn(TableName.Organization, "userTokenExpiration"))) {
|
||||||
await knex.schema.alterTable(TableName.Organization, (t) => {
|
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(),
|
privilegeUpgradeInitiatedByUsername: z.string().nullable().optional(),
|
||||||
privilegeUpgradeInitiatedAt: z.date().nullable().optional(),
|
privilegeUpgradeInitiatedAt: z.date().nullable().optional(),
|
||||||
bypassOrgAuthEnabled: z.boolean().default(false),
|
bypassOrgAuthEnabled: z.boolean().default(false),
|
||||||
userTokenExpiration: z.string().default("30d")
|
userTokenExpiration: z.string().nullable().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TOrganizations = z.infer<typeof OrganizationsSchema>;
|
export type TOrganizations = z.infer<typeof OrganizationsSchema>;
|
||||||
|
|||||||
@@ -6,4 +6,5 @@ export * from "./array";
|
|||||||
export * from "./dates";
|
export * from "./dates";
|
||||||
export * from "./object";
|
export * from "./object";
|
||||||
export * from "./string";
|
export * from "./string";
|
||||||
|
export * from "./time";
|
||||||
export * from "./undefined";
|
export * from "./undefined";
|
||||||
|
|||||||
@@ -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 { z } from "zod";
|
||||||
|
|
||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
import { getMinExpiresIn } from "@app/lib/fn";
|
||||||
import { authRateLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { authRateLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { AuthMode, AuthTokenType } from "@app/services/auth/auth-type";
|
import { AuthMode, AuthTokenType } from "@app/services/auth/auth-type";
|
||||||
@@ -79,7 +80,7 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => {
|
|||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const { decodedToken, tokenVersion } = await server.services.authToken.validateRefreshToken(req.cookies.jid);
|
const { decodedToken, tokenVersion } = await server.services.authToken.validateRefreshToken(req.cookies.jid);
|
||||||
const appCfg = getConfig();
|
const appCfg = getConfig();
|
||||||
let expiresIn = appCfg.JWT_AUTH_LIFETIME;
|
let expiresIn: string | number = appCfg.JWT_AUTH_LIFETIME;
|
||||||
if (decodedToken.organizationId) {
|
if (decodedToken.organizationId) {
|
||||||
const org = await server.services.org.findOrganizationById(
|
const org = await server.services.org.findOrganizationById(
|
||||||
decodedToken.userId,
|
decodedToken.userId,
|
||||||
@@ -87,8 +88,8 @@ export const registerAuthRoutes = async (server: FastifyZodProvider) => {
|
|||||||
decodedToken.authMethod,
|
decodedToken.authMethod,
|
||||||
decodedToken.organizationId
|
decodedToken.organizationId
|
||||||
);
|
);
|
||||||
if (org) {
|
if (org && org.userTokenExpiration) {
|
||||||
expiresIn = org.userTokenExpiration;
|
expiresIn = getMinExpiresIn(appCfg.JWT_AUTH_LIFETIME, org.userTokenExpiration);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -267,7 +267,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
|
|||||||
bypassOrgAuthEnabled: z.boolean().optional(),
|
bypassOrgAuthEnabled: z.boolean().optional(),
|
||||||
userTokenExpiration: z
|
userTokenExpiration: z
|
||||||
.string()
|
.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(
|
.refine(
|
||||||
(val) => {
|
(val) => {
|
||||||
const numericPart = val.slice(0, -1);
|
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 { infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||||
import { getUserPrivateKey } from "@app/lib/crypto/srp";
|
import { getUserPrivateKey } from "@app/lib/crypto/srp";
|
||||||
import { BadRequestError, DatabaseError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors";
|
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 { logger } from "@app/lib/logger";
|
||||||
import { getUserAgentType } from "@app/server/plugins/audit-log";
|
import { getUserAgentType } from "@app/server/plugins/audit-log";
|
||||||
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
|
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");
|
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(
|
const accessToken = jwt.sign(
|
||||||
{
|
{
|
||||||
authMethod,
|
authMethod,
|
||||||
@@ -155,7 +166,7 @@ export const authLoginServiceFactory = ({
|
|||||||
mfaMethod
|
mfaMethod
|
||||||
},
|
},
|
||||||
cfg.AUTH_SECRET,
|
cfg.AUTH_SECRET,
|
||||||
{ expiresIn: cfg.JWT_AUTH_LIFETIME }
|
{ expiresIn: tokenSessionExpiresIn }
|
||||||
);
|
);
|
||||||
|
|
||||||
const refreshToken = jwt.sign(
|
const refreshToken = jwt.sign(
|
||||||
@@ -170,7 +181,7 @@ export const authLoginServiceFactory = ({
|
|||||||
mfaMethod
|
mfaMethod
|
||||||
},
|
},
|
||||||
cfg.AUTH_SECRET,
|
cfg.AUTH_SECRET,
|
||||||
{ expiresIn: cfg.JWT_REFRESH_LIFETIME }
|
{ expiresIn: refreshTokenExpiresIn }
|
||||||
);
|
);
|
||||||
|
|
||||||
return { access: accessToken, refresh: refreshToken };
|
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 { infisicalSymmetricDecrypt, infisicalSymmetricEncypt } from "@app/lib/crypto/encryption";
|
||||||
import { generateUserSrpKeys, getUserPrivateKey } from "@app/lib/crypto/srp";
|
import { generateUserSrpKeys, getUserPrivateKey } from "@app/lib/crypto/srp";
|
||||||
import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
|
import { getMinExpiresIn } from "@app/lib/fn";
|
||||||
import { isDisposableEmail } from "@app/lib/validator";
|
import { isDisposableEmail } from "@app/lib/validator";
|
||||||
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
|
import { TGroupProjectDALFactory } from "@app/services/group-project/group-project-dal";
|
||||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||||
@@ -46,7 +47,7 @@ type TAuthSignupDep = {
|
|||||||
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser" | "findProjectById">;
|
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser" | "findProjectById">;
|
||||||
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
|
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
|
||||||
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
|
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
|
||||||
orgService: Pick<TOrgServiceFactory, "createOrganization">;
|
orgService: Pick<TOrgServiceFactory, "createOrganization" | "findOrganizationById">;
|
||||||
orgDAL: TOrgDALFactory;
|
orgDAL: TOrgDALFactory;
|
||||||
tokenService: TAuthTokenServiceFactory;
|
tokenService: TAuthTokenServiceFactory;
|
||||||
smtpService: TSmtpService;
|
smtpService: TSmtpService;
|
||||||
@@ -320,6 +321,17 @@ export const authSignupServiceFactory = ({
|
|||||||
projectBotDAL
|
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({
|
const tokenSession = await tokenService.getUserTokenSession({
|
||||||
userAgent,
|
userAgent,
|
||||||
ip,
|
ip,
|
||||||
@@ -337,7 +349,7 @@ export const authSignupServiceFactory = ({
|
|||||||
organizationId
|
organizationId
|
||||||
},
|
},
|
||||||
appCfg.AUTH_SECRET,
|
appCfg.AUTH_SECRET,
|
||||||
{ expiresIn: appCfg.JWT_AUTH_LIFETIME }
|
{ expiresIn: tokenSessionExpiresIn }
|
||||||
);
|
);
|
||||||
|
|
||||||
const refreshToken = jwt.sign(
|
const refreshToken = jwt.sign(
|
||||||
@@ -350,7 +362,7 @@ export const authSignupServiceFactory = ({
|
|||||||
organizationId
|
organizationId
|
||||||
},
|
},
|
||||||
appCfg.AUTH_SECRET,
|
appCfg.AUTH_SECRET,
|
||||||
{ expiresIn: appCfg.JWT_REFRESH_LIFETIME }
|
{ expiresIn: refreshTokenExpiresIn }
|
||||||
);
|
);
|
||||||
|
|
||||||
return { user: updateduser.info, accessToken, refreshToken, organizationId };
|
return { user: updateduser.info, accessToken, refreshToken, organizationId };
|
||||||
|
|||||||
@@ -170,8 +170,12 @@ export const orgServiceFactory = ({
|
|||||||
actorOrgId: string | undefined
|
actorOrgId: string | undefined
|
||||||
) => {
|
) => {
|
||||||
await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId);
|
await permissionService.getUserOrgPermission(userId, orgId, actorAuthMethod, actorOrgId);
|
||||||
|
const appCfg = getConfig();
|
||||||
const org = await orgDAL.findOrgById(orgId);
|
const org = await orgDAL.findOrgById(orgId);
|
||||||
if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` });
|
if (!org) throw new NotFoundError({ message: `Organization with ID '${orgId}' not found` });
|
||||||
|
if (!org.userTokenExpiration) {
|
||||||
|
return { ...org, userTokenExpiration: appCfg.JWT_REFRESH_LIFETIME };
|
||||||
|
}
|
||||||
return org;
|
return org;
|
||||||
};
|
};
|
||||||
/*
|
/*
|
||||||
|
|||||||
Reference in New Issue
Block a user