diff --git a/backend/src/controllers/v1/authController.ts b/backend/src/controllers/v1/authController.ts index 7b323309a..ddcbd6051 100644 --- a/backend/src/controllers/v1/authController.ts +++ b/backend/src/controllers/v1/authController.ts @@ -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() diff --git a/backend/src/controllers/v1/passwordController.ts b/backend/src/controllers/v1/passwordController.ts index bf8f5f152..c066bf793 100644 --- a/backend/src/controllers/v1/passwordController.ts +++ b/backend/src/controllers/v1/passwordController.ts @@ -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' diff --git a/backend/src/controllers/v2/authController.ts b/backend/src/controllers/v2/authController.ts index a4ee36aae..ff29c700a 100644 --- a/backend/src/controllers/v2/authController.ts +++ b/backend/src/controllers/v2/authController.ts @@ -235,8 +235,6 @@ export const login2 = async (req: Request, res: Response) => { } }; -import { validateUserEmail } from '../../validation'; - /** * Send MFA token to email [email] * @param req diff --git a/backend/src/controllers/v3/authController.ts b/backend/src/controllers/v3/authController.ts index 61edffb42..59e3ef043 100644 --- a/backend/src/controllers/v3/authController.ts +++ b/backend/src/controllers/v3/authController.ts @@ -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() }); diff --git a/backend/src/helpers/auth.ts b/backend/src/helpers/auth.ts index 2114f2730..be4fe4b49 100644 --- a/backend/src/helpers/auth.ts +++ b/backend/src/helpers/auth.ts @@ -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 => { // increment refreshVersion on user by 1 + + // change this await User.findOneAndUpdate({ _id: userId }, { $inc: { - refreshVersion: 1 + refreshVersion: 1, + accessVersion: 1 } }); }; diff --git a/backend/src/index.ts b/backend/src/index.ts index 76790f368..dd6642141 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -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( diff --git a/backend/src/models/index.ts b/backend/src/models/index.ts index 843296b55..cb4ee672a 100644 --- a/backend/src/models/index.ts +++ b/backend/src/models/index.ts @@ -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 }; diff --git a/backend/src/models/tokenVersion.ts b/backend/src/models/tokenVersion.ts new file mode 100644 index 000000000..c591809db --- /dev/null +++ b/backend/src/models/tokenVersion.ts @@ -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( + { + 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('TokenVersion', tokenVersionSchema); + +export default TokenVersion; \ No newline at end of file diff --git a/backend/src/models/user.ts b/backend/src/models/user.ts index 31163ebe8..1c12720f8 100644 --- a/backend/src/models/user.ts +++ b/backend/src/models/user.ts @@ -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( type: String, select: false }, - refreshVersion: { - type: Number, - default: 0, - select: false - }, isMfaEnabled: { type: Boolean, default: false diff --git a/frontend/src/components/utilities/cryptography/changePassword.ts b/frontend/src/components/utilities/cryptography/changePassword.ts index e5b8fffb5..4f1eb85c3 100644 --- a/frontend/src/components/utilities/cryptography/changePassword.ts +++ b/frontend/src/components/utilities/cryptography/changePassword.ts @@ -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);