feat(core): Custom session timeout and refresh configuration (#8342)
This commit is contained in:
@@ -667,6 +667,7 @@ export interface ILicensePostResponse extends ILicenseReadResponse {
|
||||
|
||||
export interface JwtToken {
|
||||
token: string;
|
||||
/** The amount of seconds after which the JWT will expire. **/
|
||||
expiresIn: number;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Response } from 'express';
|
||||
import { createHash } from 'crypto';
|
||||
import { AUTH_COOKIE_NAME, RESPONSE_ERROR_MESSAGES } from '@/constants';
|
||||
import { AUTH_COOKIE_NAME, RESPONSE_ERROR_MESSAGES, Time } from '@/constants';
|
||||
import type { JwtPayload, JwtToken } from '@/Interfaces';
|
||||
import type { User } from '@db/entities/User';
|
||||
import config from '@/config';
|
||||
@@ -14,7 +14,9 @@ import { ApplicationError } from 'n8n-workflow';
|
||||
|
||||
export function issueJWT(user: User): JwtToken {
|
||||
const { id, email, password } = user;
|
||||
const expiresIn = 7 * 86400000; // 7 days
|
||||
const expiresInHours = config.getEnv('userManagement.jwtSessionDurationHours');
|
||||
const expiresInSeconds = expiresInHours * Time.hours.toSeconds;
|
||||
|
||||
const isWithinUsersLimit = Container.get(License).isWithinUsersLimit();
|
||||
|
||||
const payload: JwtPayload = {
|
||||
@@ -37,12 +39,12 @@ export function issueJWT(user: User): JwtToken {
|
||||
}
|
||||
|
||||
const signedToken = Container.get(JwtService).sign(payload, {
|
||||
expiresIn: expiresIn / 1000 /* in seconds */,
|
||||
expiresIn: expiresInSeconds,
|
||||
});
|
||||
|
||||
return {
|
||||
token: signedToken,
|
||||
expiresIn,
|
||||
expiresIn: expiresInSeconds,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -86,7 +88,7 @@ export async function resolveJwt(token: string): Promise<User> {
|
||||
export async function issueCookie(res: Response, user: User): Promise<void> {
|
||||
const userData = issueJWT(user);
|
||||
res.cookie(AUTH_COOKIE_NAME, userData.token, {
|
||||
maxAge: userData.expiresIn,
|
||||
maxAge: userData.expiresIn * Time.seconds.toMilliseconds,
|
||||
httpOnly: true,
|
||||
sameSite: 'lax',
|
||||
});
|
||||
|
||||
@@ -62,9 +62,18 @@ if (!inE2ETests && !inTest) {
|
||||
});
|
||||
}
|
||||
|
||||
// Validate Configuration
|
||||
config.validate({
|
||||
allowed: 'strict',
|
||||
});
|
||||
const userManagement = config.get('userManagement');
|
||||
if (userManagement.jwtRefreshTimeoutHours >= userManagement.jwtSessionDurationHours) {
|
||||
console.warn(
|
||||
'N8N_USER_MANAGEMENT_JWT_REFRESH_TIMEOUT_HOURS needs to smaller than N8N_USER_MANAGEMENT_JWT_DURATION_HOURS. Setting N8N_USER_MANAGEMENT_JWT_REFRESH_TIMEOUT_HOURS to 0 for now.',
|
||||
);
|
||||
|
||||
config.set('userManagement.jwtRefreshTimeoutHours', 0);
|
||||
}
|
||||
|
||||
setGlobalState({
|
||||
defaultTimezone: config.getEnv('generic.timezone'),
|
||||
|
||||
@@ -762,11 +762,17 @@ export const schema = {
|
||||
default: '',
|
||||
env: 'N8N_USER_MANAGEMENT_JWT_SECRET',
|
||||
},
|
||||
jwtDuration: {
|
||||
doc: 'Set a specific JWT secret (optional - n8n can generate one)', // Generated @ start.ts
|
||||
jwtSessionDurationHours: {
|
||||
doc: 'Set a specific expiration date for the JWTs in hours.',
|
||||
format: Number,
|
||||
default: 168,
|
||||
env: 'N8N_USER_MANAGEMENT_JWT_DURATION',
|
||||
env: 'N8N_USER_MANAGEMENT_JWT_DURATION_HOURS',
|
||||
},
|
||||
jwtRefreshTimeoutHours: {
|
||||
doc: 'How long before the JWT expires to automatically refresh it. 0 means 25% of N8N_USER_MANAGEMENT_JWT_DURATION_HOURS. -1 means it will never refresh, which forces users to login again after the defined period in N8N_USER_MANAGEMENT_JWT_DURATION_HOURS.',
|
||||
format: Number,
|
||||
default: 0,
|
||||
env: 'N8N_USER_MANAGEMENT_JWT_REFRESH_TIMEOUT_HOURS',
|
||||
},
|
||||
isInstanceOwnerSetUp: {
|
||||
// n8n loads this setting from DB on startup
|
||||
|
||||
@@ -103,6 +103,7 @@ export const UM_FIX_INSTRUCTION =
|
||||
|
||||
/**
|
||||
* Units of time in milliseconds
|
||||
* @deprecated Please use constants.Time instead.
|
||||
*/
|
||||
export const TIME = {
|
||||
SECOND: 1000,
|
||||
@@ -111,6 +112,28 @@ export const TIME = {
|
||||
DAY: 24 * 60 * 60 * 1000,
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Convert time from any unit to any other unit
|
||||
*
|
||||
* Please amend conversions as necessary.
|
||||
* Eventually this will superseed `TIME` above
|
||||
*/
|
||||
export const Time = {
|
||||
seconds: {
|
||||
toMilliseconds: 1000,
|
||||
},
|
||||
minutes: {
|
||||
toMilliseconds: 60 * 1000,
|
||||
},
|
||||
hours: {
|
||||
toMilliseconds: 60 * 60 * 1000,
|
||||
toSeconds: 60 * 60,
|
||||
},
|
||||
days: {
|
||||
toSeconds: 24 * 60 * 60,
|
||||
},
|
||||
};
|
||||
|
||||
export const MIN_PASSWORD_CHAR_LENGTH = 8;
|
||||
|
||||
export const MAX_PASSWORD_CHAR_LENGTH = 64;
|
||||
|
||||
@@ -11,6 +11,7 @@ import { issueCookie, resolveJwtContent } from '@/auth/jwt';
|
||||
import { canSkipAuth } from '@/decorators/registerController';
|
||||
import { Logger } from '@/Logger';
|
||||
import { JwtService } from '@/services/jwt.service';
|
||||
import config from '@/config';
|
||||
|
||||
const jwtFromRequest = (req: Request) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
@@ -41,17 +42,29 @@ const userManagementJwtAuth = (): RequestHandler => {
|
||||
/**
|
||||
* middleware to refresh cookie before it expires
|
||||
*/
|
||||
const refreshExpiringCookie: RequestHandler = async (req: AuthenticatedRequest, res, next) => {
|
||||
export const refreshExpiringCookie = (async (req: AuthenticatedRequest, res, next) => {
|
||||
const jwtRefreshTimeoutHours = config.get('userManagement.jwtRefreshTimeoutHours');
|
||||
|
||||
let jwtRefreshTimeoutMilliSeconds: number;
|
||||
|
||||
if (jwtRefreshTimeoutHours === 0) {
|
||||
const jwtSessionDurationHours = config.get('userManagement.jwtSessionDurationHours');
|
||||
|
||||
jwtRefreshTimeoutMilliSeconds = Math.floor(jwtSessionDurationHours * 0.25 * 60 * 60 * 1000);
|
||||
} else {
|
||||
jwtRefreshTimeoutMilliSeconds = Math.floor(jwtRefreshTimeoutHours * 60 * 60 * 1000);
|
||||
}
|
||||
|
||||
const cookieAuth = jwtFromRequest(req);
|
||||
if (cookieAuth && req.user) {
|
||||
|
||||
if (cookieAuth && req.user && jwtRefreshTimeoutHours !== -1) {
|
||||
const cookieContents = jwt.decode(cookieAuth) as JwtPayload & { exp: number };
|
||||
if (cookieContents.exp * 1000 - Date.now() < 259200000) {
|
||||
// if cookie expires in < 3 days, renew it.
|
||||
if (cookieContents.exp * 1000 - Date.now() < jwtRefreshTimeoutMilliSeconds) {
|
||||
await issueCookie(res, req.user);
|
||||
}
|
||||
}
|
||||
next();
|
||||
};
|
||||
}) satisfies RequestHandler;
|
||||
|
||||
const passportMiddleware = passport.authenticate('jwt', { session: false }) as RequestHandler;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user