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

View File

@@ -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()

View File

@@ -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'

View File

@@ -235,8 +235,6 @@ export const login2 = async (req: Request, res: Response) => {
}
};
import { validateUserEmail } from '../../validation';
/**
* Send MFA token to email [email]
* @param req

View File

@@ -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() });

View File

@@ -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
}
});
};

View File

@@ -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(

View File

@@ -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
};

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;

View File

@@ -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

View File

@@ -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);