Begin preliminary tokenVersion impl

This commit is contained in:
Tuan Dang
2023-06-06 11:25:08 +01:00
parent 5a1b6acc93
commit b9dad5c3f0
10 changed files with 195 additions and 90 deletions
+24 -5
View File
@@ -4,14 +4,21 @@ import jwt from 'jsonwebtoken';
import * as bigintConversion from 'bigint-conversion'; import * as bigintConversion from 'bigint-conversion';
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
const jsrp = require('jsrp'); const jsrp = require('jsrp');
import { User, LoginSRPDetail } from '../../models'; import {
User,
LoginSRPDetail,
TokenVersion
} from '../../models';
import { createToken, issueAuthTokens, clearTokens } from '../../helpers/auth'; import { createToken, issueAuthTokens, clearTokens } from '../../helpers/auth';
import { checkUserDevice } from '../../helpers/user'; import { checkUserDevice } from '../../helpers/user';
import { import {
ACTION_LOGIN, ACTION_LOGIN,
ACTION_LOGOUT ACTION_LOGOUT
} from '../../variables'; } from '../../variables';
import { BadRequestError } from '../../utils/errors'; import {
BadRequestError,
UnauthorizedRequestError
} from '../../utils/errors';
import { EELogService } from '../../ee/services'; import { EELogService } from '../../ee/services';
import { getChannelFromUserAgent } from '../../utils/posthog'; import { getChannelFromUserAgent } from '../../utils/posthog';
import { import {
@@ -241,19 +248,31 @@ export const getNewToken = async (req: Request, res: Response) => {
const user = await User.findOne({ const user = await User.findOne({
_id: decodedToken.userId _id: decodedToken.userId
}).select('+publicKey +refreshVersion'); }).select('+publicKey +refreshVersion +accessVersion');
if (!user) throw new Error('Failed to authenticate unfound user'); if (!user) throw new Error('Failed to authenticate unfound user');
if (!user?.publicKey) if (!user?.publicKey)
throw new Error('Failed to authenticate not fully set up account'); 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' message: 'Failed to validate refresh token'
}); });
const token = createToken({ const token = createToken({
payload: { payload: {
userId: decodedToken.userId userId: decodedToken.userId,
accessVersion: tokenVersion.refreshVersion
}, },
expiresIn: await getJwtAuthLifetime(), expiresIn: await getJwtAuthLifetime(),
secret: await getJwtAuthSecret() 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', '', { res.cookie('jid', '', {
// httpOnly: true, httpOnly: true,
// path: '/', path: '/',
// sameSite: 'strict', sameSite: 'strict',
// secure: (await getHttpsEnabled()) as boolean secure: (await getHttpsEnabled()) as boolean
// }); });
return res.status(200).send({ return res.status(200).send({
message: 'Successfully changed password' 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] * Send MFA token to email [email]
* @param req * @param req
+5 -1
View File
@@ -113,7 +113,7 @@ export const login2 = async (req: Request, res: Response) => {
const user = await User.findOne({ const user = await User.findOne({
email, 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'); 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'] ?? '' 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 // issue tokens
const tokens = await issueAuthTokens({ userId: user._id.toString() }); const tokens = await issueAuthTokens({ userId: user._id.toString() });
+44 -9
View File
@@ -6,7 +6,8 @@ import {
User, User,
ServiceTokenData, ServiceTokenData,
ServiceAccount, ServiceAccount,
APIKeyData APIKeyData,
TokenVersion
} from '../models'; } from '../models';
import { import {
AccountNotFoundError, AccountNotFoundError,
@@ -108,11 +109,32 @@ export const getAuthUserPayload = async ({
const user = await User.findOne({ const user = await User.findOne({
_id: decodedToken.userId _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; return user;
} }
@@ -256,14 +278,23 @@ export const getAuthAPIKeyPayload = async ({
* @return {String} obj.refreshToken - issued refresh token * @return {String} obj.refreshToken - issued refresh token
*/ */
export const issueAuthTokens = async ({ userId }: { userId: string }) => { 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'); const tokenVersion = await new TokenVersion({
if (!user) throw AccountNotFoundError(); user: new Types.ObjectId(userId),
name: '', // improve to channel
refreshVersion: 0,
accessVersion: 0
});
// issue tokens // issue tokens
const token = createToken({ const token = createToken({
payload: { payload: {
userId userId,
tokenVersionId: tokenVersion._id.toString(),
accessVersion: tokenVersion.accessVersion
}, },
expiresIn: await getJwtAuthLifetime(), expiresIn: await getJwtAuthLifetime(),
secret: await getJwtAuthSecret() secret: await getJwtAuthSecret()
@@ -272,7 +303,8 @@ export const issueAuthTokens = async ({ userId }: { userId: string }) => {
const refreshToken = createToken({ const refreshToken = createToken({
payload: { payload: {
userId, userId,
refreshVersion: user.refreshVersion tokenVersionId: tokenVersion._id.toString(),
refreshVersion: tokenVersion.refreshVersion
}, },
expiresIn: await getJwtRefreshLifetime(), expiresIn: await getJwtRefreshLifetime(),
secret: await getJwtRefreshSecret() secret: await getJwtRefreshSecret()
@@ -291,11 +323,14 @@ export const issueAuthTokens = async ({ userId }: { userId: string }) => {
*/ */
export const clearTokens = async (userId: Types.ObjectId): Promise<void> => { export const clearTokens = async (userId: Types.ObjectId): Promise<void> => {
// increment refreshVersion on user by 1 // increment refreshVersion on user by 1
// change this
await User.findOneAndUpdate({ await User.findOneAndUpdate({
_id: userId _id: userId
}, { }, {
$inc: { $inc: {
refreshVersion: 1 refreshVersion: 1,
accessVersion: 1
} }
}); });
}; };
+69 -58
View File
@@ -1,6 +1,6 @@
import dotenv from "dotenv"; import dotenv from "dotenv";
dotenv.config(); dotenv.config();
import express, { Request, NextFunction, Response } from "express"; import express from "express";
import helmet from "helmet"; import helmet from "helmet";
import cors from "cors"; import cors from "cors";
import { DatabaseService } from "./services"; import { DatabaseService } from "./services";
@@ -9,9 +9,10 @@ import { setUpHealthEndpoint } from "./services/health";
import cookieParser from "cookie-parser"; import cookieParser from "cookie-parser";
import swaggerUi = require("swagger-ui-express"); import swaggerUi = require("swagger-ui-express");
// eslint-disable-next-line @typescript-eslint/no-var-requires // 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 // 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 { import {
workspace as eeWorkspaceRouter, workspace as eeWorkspaceRouter,
secret as eeSecretRouter, secret as eeSecretRouter,
@@ -73,81 +74,91 @@ const main = async () => {
await EELicenseService.initGlobalFeatureSet(); await EELicenseService.initGlobalFeatureSet();
const app = express(); const app = express();
app.enable('trust proxy'); app.enable("trust proxy");
app.use(express.json()); app.use(express.json());
app.use(cookieParser()); app.use(cookieParser());
app.use( app.use(
cors({ cors({
credentials: true, credentials: true,
origin: await getSiteURL() origin: await getSiteURL(),
}) })
); );
if ((await getNodeEnv()) === 'production') { app.use(requestIp.mw());
// enable app-wide rate-limiting + helmet security
// in production if ((await getNodeEnv()) === "production") {
app.disable('x-powered-by'); // enable app-wide rate-limiting + helmet security
app.use(apiLimiter); // in production
app.use(helmet()); app.disable("x-powered-by");
app.use(apiLimiter);
app.use(helmet());
} }
// (EE) routes // (EE) routes
app.use('/api/v1/secret', eeSecretRouter); app.use("/api/v1/secret", eeSecretRouter);
app.use('/api/v1/secret-snapshot', eeSecretSnapshotRouter); app.use("/api/v1/secret-snapshot", eeSecretSnapshotRouter);
app.use('/api/v1/workspace', eeWorkspaceRouter); app.use("/api/v1/workspace", eeWorkspaceRouter);
app.use('/api/v1/action', eeActionRouter); app.use("/api/v1/action", eeActionRouter);
app.use('/api/v1/organizations', eeOrganizationsRouter); app.use("/api/v1/organizations", eeOrganizationsRouter);
app.use('/api/v1/cloud-products', eeCloudProductsRouter); app.use("/api/v1/cloud-products", eeCloudProductsRouter);
// v1 routes (default) // v1 routes (default)
app.use('/api/v1/signup', v1SignupRouter); app.use("/api/v1/signup", v1SignupRouter);
app.use('/api/v1/auth', v1AuthRouter); app.use("/api/v1/auth", v1AuthRouter);
app.use('/api/v1/bot', v1BotRouter); app.use("/api/v1/bot", v1BotRouter);
app.use('/api/v1/user', v1UserRouter); app.use("/api/v1/user", v1UserRouter);
app.use('/api/v1/user-action', v1UserActionRouter); app.use("/api/v1/user-action", v1UserActionRouter);
app.use('/api/v1/organization', v1OrganizationRouter); app.use("/api/v1/organization", v1OrganizationRouter);
app.use('/api/v1/workspace', v1WorkspaceRouter); app.use("/api/v1/workspace", v1WorkspaceRouter);
app.use('/api/v1/membership-org', v1MembershipOrgRouter); app.use("/api/v1/membership-org", v1MembershipOrgRouter);
app.use('/api/v1/membership', v1MembershipRouter); app.use("/api/v1/membership", v1MembershipRouter);
app.use('/api/v1/key', v1KeyRouter); app.use("/api/v1/key", v1KeyRouter);
app.use('/api/v1/invite-org', v1InviteOrgRouter); app.use("/api/v1/invite-org", v1InviteOrgRouter);
app.use('/api/v1/secret', v1SecretRouter); // deprecate app.use("/api/v1/secret", v1SecretRouter); // deprecate
app.use('/api/v1/service-token', v1ServiceTokenRouter); // deprecate app.use("/api/v1/service-token", v1ServiceTokenRouter); // deprecate
app.use('/api/v1/password', v1PasswordRouter); app.use("/api/v1/password", v1PasswordRouter);
app.use('/api/v1/stripe', v1StripeRouter); app.use("/api/v1/stripe", v1StripeRouter);
app.use('/api/v1/integration', v1IntegrationRouter); app.use("/api/v1/integration", v1IntegrationRouter);
app.use('/api/v1/integration-auth', v1IntegrationAuthRouter); app.use("/api/v1/integration-auth", v1IntegrationAuthRouter);
app.use('/api/v1/folder', v1SecretsFolder) app.use("/api/v1/folders", v1SecretsFolder);
// v2 routes (improvements) // v2 routes (improvements)
app.use('/api/v2/signup', v2SignupRouter); app.use("/api/v2/signup", v2SignupRouter);
app.use('/api/v2/auth', v2AuthRouter); app.use("/api/v2/auth", v2AuthRouter);
app.use('/api/v2/users', v2UsersRouter); app.use("/api/v2/users", v2UsersRouter);
app.use('/api/v2/organizations', v2OrganizationsRouter); app.use("/api/v2/organizations", v2OrganizationsRouter);
app.use('/api/v2/workspace', v2EnvironmentRouter); app.use("/api/v2/workspace", v2EnvironmentRouter);
app.use('/api/v2/workspace', v2TagsRouter); app.use("/api/v2/workspace", v2TagsRouter);
app.use('/api/v2/workspace', v2WorkspaceRouter); app.use("/api/v2/workspace", v2WorkspaceRouter);
app.use('/api/v2/secret', v2SecretRouter); // deprecate 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/secrets", v2SecretsRouter); // note: in the process of moving to v3/secrets
app.use('/api/v2/service-token', v2ServiceTokenDataRouter); app.use("/api/v2/service-token", v2ServiceTokenDataRouter);
app.use('/api/v2/service-accounts', v2ServiceAccountsRouter); // new app.use("/api/v2/service-accounts", v2ServiceAccountsRouter); // new
app.use('/api/v2/api-key', v2APIKeyDataRouter); app.use("/api/v2/api-key", v2APIKeyDataRouter);
// v3 routes (experimental) // v3 routes (experimental)
app.use('/api/v3/secrets', v3SecretsRouter); app.use("/api/v3/auth", v3AuthRouter);
app.use('/api/v3/workspaces', v3WorkspacesRouter); app.use("/api/v3/secrets", v3SecretsRouter);
app.use("/api/v3/workspaces", v3WorkspacesRouter);
app.use("/api/v3/signup", v3SignupRouter);
// api docs // api docs
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerFile)) app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(swaggerFile));
// server status // server status
app.use('/api', healthCheck) app.use("/api", healthCheck);
//* Handle unrouted requests and respond with proper error message as well as status code //* 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(); 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 () => { const server = app.listen(await getPort(), async () => {
(await getLogger("backend-main")).info( (await getLogger("backend-main")).info(
+4 -1
View File
@@ -22,6 +22,7 @@ import Workspace, { IWorkspace } from './workspace';
import ServiceTokenData, { IServiceTokenData } from './serviceTokenData'; import ServiceTokenData, { IServiceTokenData } from './serviceTokenData';
import APIKeyData, { IAPIKeyData } from './apiKeyData'; import APIKeyData, { IAPIKeyData } from './apiKeyData';
import LoginSRPDetail, { ILoginSRPDetail } from './loginSRPDetail'; import LoginSRPDetail, { ILoginSRPDetail } from './loginSRPDetail';
import TokenVersion, { ITokenVersion } from './tokenVersion';
export { export {
AuthProvider, AuthProvider,
@@ -72,5 +73,7 @@ export {
APIKeyData, APIKeyData,
IAPIKeyData, IAPIKeyData,
LoginSRPDetail, LoginSRPDetail,
ILoginSRPDetail ILoginSRPDetail,
TokenVersion,
ITokenVersion
}; };
+37
View 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;
-6
View File
@@ -21,7 +21,6 @@ export interface IUser extends Document {
tag?: string; tag?: string;
salt?: string; salt?: string;
verifier?: string; verifier?: string;
refreshVersion: number;
isMfaEnabled: boolean; isMfaEnabled: boolean;
mfaMethods: boolean; mfaMethods: boolean;
devices: { devices: {
@@ -91,11 +90,6 @@ const userSchema = new Schema<IUser>(
type: String, type: String,
select: false select: false
}, },
refreshVersion: {
type: Number,
default: 0,
select: false
},
isMfaEnabled: { isMfaEnabled: {
type: Boolean, type: Boolean,
default: false default: false
@@ -125,6 +125,10 @@ const changePassword = async (
setPasswordChanged(true); setPasswordChanged(true);
setCurrentPassword(''); setCurrentPassword('');
setNewPassword(''); setNewPassword('');
window.location.href = '/login';
// move to login page
} catch (error) { } catch (error) {
setCurrentPasswordError(true); setCurrentPasswordError(true);
console.log(error); console.log(error);