mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Begin preliminary tokenVersion impl
This commit is contained in:
@@ -4,14 +4,21 @@ import jwt from 'jsonwebtoken';
|
||||
import * as bigintConversion from 'bigint-conversion';
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const jsrp = require('jsrp');
|
||||
import { User, LoginSRPDetail } from '../../models';
|
||||
import {
|
||||
User,
|
||||
LoginSRPDetail,
|
||||
TokenVersion
|
||||
} from '../../models';
|
||||
import { createToken, issueAuthTokens, clearTokens } from '../../helpers/auth';
|
||||
import { checkUserDevice } from '../../helpers/user';
|
||||
import {
|
||||
ACTION_LOGIN,
|
||||
ACTION_LOGOUT
|
||||
} from '../../variables';
|
||||
import { BadRequestError } from '../../utils/errors';
|
||||
import {
|
||||
BadRequestError,
|
||||
UnauthorizedRequestError
|
||||
} from '../../utils/errors';
|
||||
import { EELogService } from '../../ee/services';
|
||||
import { getChannelFromUserAgent } from '../../utils/posthog';
|
||||
import {
|
||||
@@ -241,19 +248,31 @@ export const getNewToken = async (req: Request, res: Response) => {
|
||||
|
||||
const user = await User.findOne({
|
||||
_id: decodedToken.userId
|
||||
}).select('+publicKey +refreshVersion');
|
||||
}).select('+publicKey +refreshVersion +accessVersion');
|
||||
|
||||
if (!user) throw new Error('Failed to authenticate unfound user');
|
||||
if (!user?.publicKey)
|
||||
throw new Error('Failed to authenticate not fully set up account');
|
||||
|
||||
const tokenVersion = await TokenVersion.findOne({
|
||||
_id: decodedToken.tokenVersionId,
|
||||
user: user._id
|
||||
});
|
||||
|
||||
if (decodedToken?.refreshVersion !== user.refreshVersion) throw BadRequestError({
|
||||
console.log('tokenVersion: ', tokenVersion);
|
||||
|
||||
if (!tokenVersion) throw UnauthorizedRequestError({
|
||||
message: 'Failed to validate refresh token'
|
||||
});
|
||||
|
||||
if (decodedToken.refreshVersion !== tokenVersion.refreshVersion) throw BadRequestError({
|
||||
message: 'Failed to validate refresh token'
|
||||
});
|
||||
|
||||
const token = createToken({
|
||||
payload: {
|
||||
userId: decodedToken.userId
|
||||
userId: decodedToken.userId,
|
||||
accessVersion: tokenVersion.refreshVersion
|
||||
},
|
||||
expiresIn: await getJwtAuthLifetime(),
|
||||
secret: await getJwtAuthSecret()
|
||||
|
||||
@@ -231,16 +231,16 @@ export const changePassword = async (req: Request, res: Response) => {
|
||||
}
|
||||
);
|
||||
|
||||
// await clearTokens(user._id);
|
||||
await clearTokens(user._id);
|
||||
|
||||
// // clear httpOnly cookie
|
||||
// clear httpOnly cookie
|
||||
|
||||
// res.cookie('jid', '', {
|
||||
// httpOnly: true,
|
||||
// path: '/',
|
||||
// sameSite: 'strict',
|
||||
// secure: (await getHttpsEnabled()) as boolean
|
||||
// });
|
||||
res.cookie('jid', '', {
|
||||
httpOnly: true,
|
||||
path: '/',
|
||||
sameSite: 'strict',
|
||||
secure: (await getHttpsEnabled()) as boolean
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
message: 'Successfully changed password'
|
||||
|
||||
@@ -235,8 +235,6 @@ export const login2 = async (req: Request, res: Response) => {
|
||||
}
|
||||
};
|
||||
|
||||
import { validateUserEmail } from '../../validation';
|
||||
|
||||
/**
|
||||
* Send MFA token to email [email]
|
||||
* @param req
|
||||
|
||||
@@ -113,7 +113,7 @@ export const login2 = async (req: Request, res: Response) => {
|
||||
|
||||
const user = await User.findOne({
|
||||
email,
|
||||
}).select('+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag');
|
||||
}).select('+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag +devices');
|
||||
|
||||
if (!user) throw new Error('Failed to find user');
|
||||
|
||||
@@ -183,6 +183,10 @@ export const login2 = async (req: Request, res: Response) => {
|
||||
userAgent: req.headers['user-agent'] ?? ''
|
||||
});
|
||||
|
||||
console.log('logged in, issue tokens');
|
||||
console.log('ip: ', req.ip);
|
||||
console.log('userAgent: ', req.headers['user-agent']);
|
||||
|
||||
// issue tokens
|
||||
const tokens = await issueAuthTokens({ userId: user._id.toString() });
|
||||
|
||||
|
||||
@@ -6,7 +6,8 @@ import {
|
||||
User,
|
||||
ServiceTokenData,
|
||||
ServiceAccount,
|
||||
APIKeyData
|
||||
APIKeyData,
|
||||
TokenVersion
|
||||
} from '../models';
|
||||
import {
|
||||
AccountNotFoundError,
|
||||
@@ -108,11 +109,32 @@ export const getAuthUserPayload = async ({
|
||||
|
||||
const user = await User.findOne({
|
||||
_id: decodedToken.userId
|
||||
}).select('+publicKey');
|
||||
}).select('+publicKey +accessVersion');
|
||||
|
||||
if (!user) throw AccountNotFoundError({ message: 'Failed to find User' });
|
||||
if (!user) throw AccountNotFoundError({ message: 'Failed to find user' });
|
||||
|
||||
if (!user?.publicKey) throw UnauthorizedRequestError({ message: 'Failed to authenticate User with partially set up account' });
|
||||
if (!user?.publicKey) throw UnauthorizedRequestError({ message: 'Failed to authenticate user with partially set up account' });
|
||||
|
||||
console.log('getAuthUserPayload');
|
||||
|
||||
const tokenVersion = await TokenVersion.findOne({
|
||||
_id: decodedToken.tokenVersionId,
|
||||
user: user._id
|
||||
});
|
||||
|
||||
console.log('tokenVersion: ', tokenVersion);
|
||||
|
||||
if (!tokenVersion) throw UnauthorizedRequestError({
|
||||
message: 'Failed to validate access token'
|
||||
});
|
||||
|
||||
if (decodedToken.accessVersion !== tokenVersion.accessVersion) {
|
||||
console.log('incorrect version');
|
||||
|
||||
throw UnauthorizedRequestError({
|
||||
message: 'Failed to validate access token'
|
||||
});
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
@@ -256,14 +278,23 @@ export const getAuthAPIKeyPayload = async ({
|
||||
* @return {String} obj.refreshToken - issued refresh token
|
||||
*/
|
||||
export const issueAuthTokens = async ({ userId }: { userId: string }) => {
|
||||
|
||||
// TODO: create tokenVersion here
|
||||
// TODO: include some kind of (channel) name here
|
||||
|
||||
const user = await User.findById(userId).select('+refreshVersion');
|
||||
if (!user) throw AccountNotFoundError();
|
||||
const tokenVersion = await new TokenVersion({
|
||||
user: new Types.ObjectId(userId),
|
||||
name: '', // improve to channel
|
||||
refreshVersion: 0,
|
||||
accessVersion: 0
|
||||
});
|
||||
|
||||
// issue tokens
|
||||
const token = createToken({
|
||||
payload: {
|
||||
userId
|
||||
userId,
|
||||
tokenVersionId: tokenVersion._id.toString(),
|
||||
accessVersion: tokenVersion.accessVersion
|
||||
},
|
||||
expiresIn: await getJwtAuthLifetime(),
|
||||
secret: await getJwtAuthSecret()
|
||||
@@ -272,7 +303,8 @@ export const issueAuthTokens = async ({ userId }: { userId: string }) => {
|
||||
const refreshToken = createToken({
|
||||
payload: {
|
||||
userId,
|
||||
refreshVersion: user.refreshVersion
|
||||
tokenVersionId: tokenVersion._id.toString(),
|
||||
refreshVersion: tokenVersion.refreshVersion
|
||||
},
|
||||
expiresIn: await getJwtRefreshLifetime(),
|
||||
secret: await getJwtRefreshSecret()
|
||||
@@ -291,11 +323,14 @@ export const issueAuthTokens = async ({ userId }: { userId: string }) => {
|
||||
*/
|
||||
export const clearTokens = async (userId: Types.ObjectId): Promise<void> => {
|
||||
// increment refreshVersion on user by 1
|
||||
|
||||
// change this
|
||||
await User.findOneAndUpdate({
|
||||
_id: userId
|
||||
}, {
|
||||
$inc: {
|
||||
refreshVersion: 1
|
||||
refreshVersion: 1,
|
||||
accessVersion: 1
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import dotenv from "dotenv";
|
||||
dotenv.config();
|
||||
import express, { Request, NextFunction, Response } from "express";
|
||||
import express from "express";
|
||||
import helmet from "helmet";
|
||||
import cors from "cors";
|
||||
import { DatabaseService } from "./services";
|
||||
@@ -9,9 +9,10 @@ import { setUpHealthEndpoint } from "./services/health";
|
||||
import cookieParser from "cookie-parser";
|
||||
import swaggerUi = require("swagger-ui-express");
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const swaggerFile = require('../spec.json');
|
||||
const swaggerFile = require("../spec.json");
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
import { apiLimiter } from './helpers/rateLimiter';
|
||||
const requestIp = require("request-ip");
|
||||
import { apiLimiter } from "./helpers/rateLimiter";
|
||||
import {
|
||||
workspace as eeWorkspaceRouter,
|
||||
secret as eeSecretRouter,
|
||||
@@ -73,81 +74,91 @@ const main = async () => {
|
||||
await EELicenseService.initGlobalFeatureSet();
|
||||
|
||||
const app = express();
|
||||
app.enable('trust proxy');
|
||||
app.enable("trust proxy");
|
||||
app.use(express.json());
|
||||
app.use(cookieParser());
|
||||
app.use(
|
||||
cors({
|
||||
credentials: true,
|
||||
origin: await getSiteURL()
|
||||
})
|
||||
cors({
|
||||
credentials: true,
|
||||
origin: await getSiteURL(),
|
||||
})
|
||||
);
|
||||
|
||||
if ((await getNodeEnv()) === 'production') {
|
||||
// enable app-wide rate-limiting + helmet security
|
||||
// in production
|
||||
app.disable('x-powered-by');
|
||||
app.use(apiLimiter);
|
||||
app.use(helmet());
|
||||
app.use(requestIp.mw());
|
||||
|
||||
if ((await getNodeEnv()) === "production") {
|
||||
// enable app-wide rate-limiting + helmet security
|
||||
// in production
|
||||
app.disable("x-powered-by");
|
||||
app.use(apiLimiter);
|
||||
app.use(helmet());
|
||||
}
|
||||
|
||||
// (EE) routes
|
||||
app.use('/api/v1/secret', eeSecretRouter);
|
||||
app.use('/api/v1/secret-snapshot', eeSecretSnapshotRouter);
|
||||
app.use('/api/v1/workspace', eeWorkspaceRouter);
|
||||
app.use('/api/v1/action', eeActionRouter);
|
||||
app.use('/api/v1/organizations', eeOrganizationsRouter);
|
||||
app.use('/api/v1/cloud-products', eeCloudProductsRouter);
|
||||
app.use("/api/v1/secret", eeSecretRouter);
|
||||
app.use("/api/v1/secret-snapshot", eeSecretSnapshotRouter);
|
||||
app.use("/api/v1/workspace", eeWorkspaceRouter);
|
||||
app.use("/api/v1/action", eeActionRouter);
|
||||
app.use("/api/v1/organizations", eeOrganizationsRouter);
|
||||
app.use("/api/v1/cloud-products", eeCloudProductsRouter);
|
||||
|
||||
// v1 routes (default)
|
||||
app.use('/api/v1/signup', v1SignupRouter);
|
||||
app.use('/api/v1/auth', v1AuthRouter);
|
||||
app.use('/api/v1/bot', v1BotRouter);
|
||||
app.use('/api/v1/user', v1UserRouter);
|
||||
app.use('/api/v1/user-action', v1UserActionRouter);
|
||||
app.use('/api/v1/organization', v1OrganizationRouter);
|
||||
app.use('/api/v1/workspace', v1WorkspaceRouter);
|
||||
app.use('/api/v1/membership-org', v1MembershipOrgRouter);
|
||||
app.use('/api/v1/membership', v1MembershipRouter);
|
||||
app.use('/api/v1/key', v1KeyRouter);
|
||||
app.use('/api/v1/invite-org', v1InviteOrgRouter);
|
||||
app.use('/api/v1/secret', v1SecretRouter); // deprecate
|
||||
app.use('/api/v1/service-token', v1ServiceTokenRouter); // deprecate
|
||||
app.use('/api/v1/password', v1PasswordRouter);
|
||||
app.use('/api/v1/stripe', v1StripeRouter);
|
||||
app.use('/api/v1/integration', v1IntegrationRouter);
|
||||
app.use('/api/v1/integration-auth', v1IntegrationAuthRouter);
|
||||
app.use('/api/v1/folder', v1SecretsFolder)
|
||||
app.use("/api/v1/signup", v1SignupRouter);
|
||||
app.use("/api/v1/auth", v1AuthRouter);
|
||||
app.use("/api/v1/bot", v1BotRouter);
|
||||
app.use("/api/v1/user", v1UserRouter);
|
||||
app.use("/api/v1/user-action", v1UserActionRouter);
|
||||
app.use("/api/v1/organization", v1OrganizationRouter);
|
||||
app.use("/api/v1/workspace", v1WorkspaceRouter);
|
||||
app.use("/api/v1/membership-org", v1MembershipOrgRouter);
|
||||
app.use("/api/v1/membership", v1MembershipRouter);
|
||||
app.use("/api/v1/key", v1KeyRouter);
|
||||
app.use("/api/v1/invite-org", v1InviteOrgRouter);
|
||||
app.use("/api/v1/secret", v1SecretRouter); // deprecate
|
||||
app.use("/api/v1/service-token", v1ServiceTokenRouter); // deprecate
|
||||
app.use("/api/v1/password", v1PasswordRouter);
|
||||
app.use("/api/v1/stripe", v1StripeRouter);
|
||||
app.use("/api/v1/integration", v1IntegrationRouter);
|
||||
app.use("/api/v1/integration-auth", v1IntegrationAuthRouter);
|
||||
app.use("/api/v1/folders", v1SecretsFolder);
|
||||
|
||||
// v2 routes (improvements)
|
||||
app.use('/api/v2/signup', v2SignupRouter);
|
||||
app.use('/api/v2/auth', v2AuthRouter);
|
||||
app.use('/api/v2/users', v2UsersRouter);
|
||||
app.use('/api/v2/organizations', v2OrganizationsRouter);
|
||||
app.use('/api/v2/workspace', v2EnvironmentRouter);
|
||||
app.use('/api/v2/workspace', v2TagsRouter);
|
||||
app.use('/api/v2/workspace', v2WorkspaceRouter);
|
||||
app.use('/api/v2/secret', v2SecretRouter); // deprecate
|
||||
app.use('/api/v2/secrets', v2SecretsRouter); // note: in the process of moving to v3/secrets
|
||||
app.use('/api/v2/service-token', v2ServiceTokenDataRouter);
|
||||
app.use('/api/v2/service-accounts', v2ServiceAccountsRouter); // new
|
||||
app.use('/api/v2/api-key', v2APIKeyDataRouter);
|
||||
app.use("/api/v2/signup", v2SignupRouter);
|
||||
app.use("/api/v2/auth", v2AuthRouter);
|
||||
app.use("/api/v2/users", v2UsersRouter);
|
||||
app.use("/api/v2/organizations", v2OrganizationsRouter);
|
||||
app.use("/api/v2/workspace", v2EnvironmentRouter);
|
||||
app.use("/api/v2/workspace", v2TagsRouter);
|
||||
app.use("/api/v2/workspace", v2WorkspaceRouter);
|
||||
app.use("/api/v2/secret", v2SecretRouter); // deprecate
|
||||
app.use("/api/v2/secrets", v2SecretsRouter); // note: in the process of moving to v3/secrets
|
||||
app.use("/api/v2/service-token", v2ServiceTokenDataRouter);
|
||||
app.use("/api/v2/service-accounts", v2ServiceAccountsRouter); // new
|
||||
app.use("/api/v2/api-key", v2APIKeyDataRouter);
|
||||
|
||||
// v3 routes (experimental)
|
||||
app.use('/api/v3/secrets', v3SecretsRouter);
|
||||
app.use('/api/v3/workspaces', v3WorkspacesRouter);
|
||||
app.use("/api/v3/auth", v3AuthRouter);
|
||||
app.use("/api/v3/secrets", v3SecretsRouter);
|
||||
app.use("/api/v3/workspaces", v3WorkspacesRouter);
|
||||
app.use("/api/v3/signup", v3SignupRouter);
|
||||
|
||||
// api docs
|
||||
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerFile))
|
||||
// api docs
|
||||
app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(swaggerFile));
|
||||
|
||||
// server status
|
||||
app.use('/api', healthCheck)
|
||||
app.use("/api", healthCheck);
|
||||
|
||||
//* Handle unrouted requests and respond with proper error message as well as status code
|
||||
app.use((req: Request, res: Response, next: NextFunction) => {
|
||||
app.use((req, res, next) => {
|
||||
if (res.headersSent) return next();
|
||||
next(RouteNotFoundError({ message: `The requested source '(${req.method})${req.url}' was not found` }))
|
||||
})
|
||||
next(
|
||||
RouteNotFoundError({
|
||||
message: `The requested source '(${req.method})${req.url}' was not found`,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
app.use(requestErrorHandler);
|
||||
|
||||
const server = app.listen(await getPort(), async () => {
|
||||
(await getLogger("backend-main")).info(
|
||||
|
||||
@@ -22,6 +22,7 @@ import Workspace, { IWorkspace } from './workspace';
|
||||
import ServiceTokenData, { IServiceTokenData } from './serviceTokenData';
|
||||
import APIKeyData, { IAPIKeyData } from './apiKeyData';
|
||||
import LoginSRPDetail, { ILoginSRPDetail } from './loginSRPDetail';
|
||||
import TokenVersion, { ITokenVersion } from './tokenVersion';
|
||||
|
||||
export {
|
||||
AuthProvider,
|
||||
@@ -72,5 +73,7 @@ export {
|
||||
APIKeyData,
|
||||
IAPIKeyData,
|
||||
LoginSRPDetail,
|
||||
ILoginSRPDetail
|
||||
ILoginSRPDetail,
|
||||
TokenVersion,
|
||||
ITokenVersion
|
||||
};
|
||||
|
||||
37
backend/src/models/tokenVersion.ts
Normal file
37
backend/src/models/tokenVersion.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { Schema, model, Types, Document } from 'mongoose';
|
||||
|
||||
export interface ITokenVersion extends Document {
|
||||
user: Types.ObjectId;
|
||||
name: string;
|
||||
refreshVersion: number;
|
||||
accessVersion: number;
|
||||
}
|
||||
|
||||
const tokenVersionSchema = new Schema<ITokenVersion>(
|
||||
{
|
||||
user: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
refreshVersion: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
accessVersion: {
|
||||
type: Number,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
{
|
||||
timestamps: true
|
||||
}
|
||||
);
|
||||
|
||||
const TokenVersion = model<ITokenVersion>('TokenVersion', tokenVersionSchema);
|
||||
|
||||
export default TokenVersion;
|
||||
@@ -21,7 +21,6 @@ export interface IUser extends Document {
|
||||
tag?: string;
|
||||
salt?: string;
|
||||
verifier?: string;
|
||||
refreshVersion: number;
|
||||
isMfaEnabled: boolean;
|
||||
mfaMethods: boolean;
|
||||
devices: {
|
||||
@@ -91,11 +90,6 @@ const userSchema = new Schema<IUser>(
|
||||
type: String,
|
||||
select: false
|
||||
},
|
||||
refreshVersion: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
select: false
|
||||
},
|
||||
isMfaEnabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
|
||||
@@ -125,6 +125,10 @@ const changePassword = async (
|
||||
setPasswordChanged(true);
|
||||
setCurrentPassword('');
|
||||
setNewPassword('');
|
||||
|
||||
window.location.href = '/login';
|
||||
|
||||
// move to login page
|
||||
} catch (error) {
|
||||
setCurrentPasswordError(true);
|
||||
console.log(error);
|
||||
|
||||
Reference in New Issue
Block a user