Finish preliminary refactor/modularization of requireAuth logic

This commit is contained in:
Tuan Dang
2023-10-27 12:03:25 +01:00
parent 52bcee2785
commit 44ec88acd6
23 changed files with 481 additions and 435 deletions

View File

@@ -24,6 +24,9 @@ import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/auth";
declare module "jsonwebtoken" {
export interface AuthnJwtPayload extends jwt.JwtPayload {
authTokenType: AuthTokenType;
}
export interface UserIDJwtPayload extends jwt.JwtPayload {
userId: string;
refreshVersion?: number;

View File

@@ -1,396 +1,13 @@
import { Request } from "express";
import { Types } from "mongoose";
import jwt from "jsonwebtoken";
import bcrypt from "bcrypt";
import {
APIKeyData,
APIKeyDataV2,
ITokenVersion,
IUser,
ServiceTokenData,
ServiceTokenDataV3,
TokenVersion,
User,
} from "../models";
import {
APIKeyDataNotFoundError,
AccountNotFoundError,
BadRequestError,
ServiceTokenDataNotFoundError,
UnauthorizedRequestError,
} from "../utils/errors";
import { ITokenVersion, TokenVersion } from "../models";
import { UnauthorizedRequestError } from "../utils/errors";
import {
getAuthSecret,
getJwtAuthLifetime,
getJwtRefreshLifetime
} from "../config";
import {
AuthMode,
AuthTokenType
} from "../variables";
import {
ServiceTokenAuthData,
ServiceTokenV3AuthData,
UserAuthData
} from "../interfaces/middleware";
import { ActorType } from "../ee/models";
import { getUserAgentType } from "../utils/posthog";
/**
*
* @param {Object} obj
* @param {Object} obj.headers - HTTP request headers object
*/
export const validateAuthMode = async ({
headers,
acceptedAuthModes,
}: {
headers: { [key: string]: string | string[] | undefined },
acceptedAuthModes: AuthMode[]
}) => {
const apiKey = headers["x-api-key"];
const authHeader = headers["authorization"];
let authMode, authTokenValue;
if (apiKey === undefined && authHeader === undefined) {
// case: no auth or X-API-KEY header present
throw BadRequestError({ message: "Missing Authorization or X-API-KEY in request header." });
}
if (typeof apiKey === "string") {
// case: treat request authentication type as via X-API-KEY (i.e. API Key)
authMode = AuthMode.API_KEY;
authTokenValue = apiKey;
}
if (typeof authHeader === "string") {
// case: treat request authentication type as via Authorization header (i.e. either JWT or service token)
const [tokenType, tokenValue] = <[string, string]>authHeader.split(" ", 2) ?? [null, null]
if (tokenType === null)
throw BadRequestError({ message: "Missing Authorization Header in the request header." });
if (tokenType.toLowerCase() !== "bearer")
throw BadRequestError({ message: `The provided authentication type '${tokenType}' is not supported.` });
if (tokenValue === null)
throw BadRequestError({ message: "Missing Authorization Body in the request header." });
const parts = tokenValue.split(".");
switch (parts[0]) {
case "st":
authMode = AuthMode.SERVICE_TOKEN;
authTokenValue = tokenValue;
break;
default: {
const decodedToken = <jwt.UserIDJwtPayload>(
jwt.verify(tokenValue, await getAuthSecret())
);
if (decodedToken.authTokenType === AuthTokenType.SERVICE_ACCESS_TOKEN) {
authMode = AuthMode.SERVICE_ACCESS_TOKEN;
} else {
authMode = AuthMode.JWT;
}
authTokenValue = tokenValue;
}
}
}
if (!authMode || !authTokenValue) throw BadRequestError({ message: "Missing valid Authorization or X-API-KEY in request header." });
if (!acceptedAuthModes.includes(authMode)) throw BadRequestError({ message: "The provided authentication type is not supported." });
return ({
authMode,
authTokenValue,
});
}
/**
* Return user payload corresponding to JWT token [authTokenValue]
* that is either for browser / CLI or API Key
* @param {Object} obj
* @param {String} obj.authTokenValue - JWT token value
* @returns {User} user - user corresponding to JWT token
*/
export const getAuthUserPayload = async ({
req,
authTokenValue,
}: {
req: Request,
authTokenValue: string;
}): Promise<UserAuthData> => {
const decodedToken = <jwt.UserIDJwtPayload>(
jwt.verify(authTokenValue, await getAuthSecret())
);
if (
decodedToken.authTokenType !== AuthTokenType.ACCESS_TOKEN &&
decodedToken.authTokenType !== AuthTokenType.API_KEY
) {
throw UnauthorizedRequestError();
}
if (decodedToken.authTokenType === AuthTokenType.ACCESS_TOKEN) {
const tokenVersion = await TokenVersion.findOneAndUpdate({
_id: new Types.ObjectId(decodedToken.tokenVersionId),
user: decodedToken.userId
}, {
lastUsed: new Date(),
});
if (!tokenVersion) throw UnauthorizedRequestError();
if (decodedToken.accessVersion !== tokenVersion.accessVersion) throw UnauthorizedRequestError();
} else if (decodedToken.authTokenType === AuthTokenType.API_KEY) {
const apiKeyData = await APIKeyDataV2.findOneAndUpdate(
{
_id: new Types.ObjectId(decodedToken.apiKeyDataId),
user: new Types.ObjectId(decodedToken.userId)
},
{
lastUsed: new Date(),
$inc: { usageCount: 1 }
},
{
new: true
}
);
if (!apiKeyData) throw UnauthorizedRequestError();
}
const user = await User.findOne({
_id: new Types.ObjectId(decodedToken.userId),
}).select("+publicKey +accessVersion");
if (!user) throw AccountNotFoundError({ message: "Failed to find user" });
if (!user?.publicKey) throw UnauthorizedRequestError({ message: "Failed to authenticate user with partially set up account" });
return {
actor: {
type: ActorType.USER,
metadata: {
userId: user._id.toString(),
email: user.email
}
},
authPayload: user,
ipAddress: req.realIP,
userAgent: req.headers["user-agent"] ?? "",
userAgentType: getUserAgentType(req.headers["user-agent"])
}
}
/**
* Return service token data payload corresponding to service token [authTokenValue]
* @param {Object} obj
* @param {String} obj.authTokenValue - service token value
* @returns {ServiceTokenData} serviceTokenData - service token data
*/
export const getAuthSTDPayload = async ({
req,
authTokenValue,
}: {
req: Request,
authTokenValue: string;
}): Promise<ServiceTokenAuthData> => {
const [_, TOKEN_IDENTIFIER, TOKEN_SECRET] = <[string, string, string]>authTokenValue.split(".", 3);
const serviceTokenData = await ServiceTokenData
.findById(TOKEN_IDENTIFIER, "+secretHash +expiresAt")
if (!serviceTokenData) {
throw ServiceTokenDataNotFoundError({ message: "Failed to find service token data" });
} else if (serviceTokenData?.expiresAt && new Date(serviceTokenData.expiresAt) < new Date()) {
// case: service token expired
await ServiceTokenData.findByIdAndDelete(serviceTokenData._id);
throw UnauthorizedRequestError({
message: "Failed to authenticate expired service token",
});
}
const isMatch = await bcrypt.compare(TOKEN_SECRET, serviceTokenData.secretHash);
if (!isMatch) throw UnauthorizedRequestError({
message: "Failed to authenticate service token",
});
const serviceTokenDataToReturn = await ServiceTokenData
.findOneAndUpdate({
_id: new Types.ObjectId(TOKEN_IDENTIFIER),
}, {
lastUsed: new Date(),
}, {
new: true,
})
.select("+encryptedKey +iv +tag")
if (!serviceTokenDataToReturn) throw ServiceTokenDataNotFoundError({ message: "Failed to find service token data" });
return {
actor: {
type: ActorType.SERVICE,
metadata: {
serviceId: serviceTokenDataToReturn._id.toString(),
name: serviceTokenDataToReturn.name
}
},
authPayload: serviceTokenDataToReturn,
ipAddress: req.realIP,
userAgent: req.headers["user-agent"] ?? "",
userAgentType: getUserAgentType(req.headers["user-agent"])
}
}
/**
* Return service token data V3 payload corresponding to service token [authTokenValue]
* @param {Object} obj
* @param {String} obj.authTokenValue - service token value
* @returns {ServiceTokenData} serviceTokenData - service token data
*/
export const getAuthSTDV3Payload = async ({
req,
authTokenValue,
}: {
req: Request,
authTokenValue: string;
}): Promise<ServiceTokenV3AuthData> => {
const decodedToken = <jwt.ServiceRefreshTokenJwtPayload>(
jwt.verify(authTokenValue, await getAuthSecret())
);
if (decodedToken.authTokenType !== AuthTokenType.SERVICE_ACCESS_TOKEN) throw UnauthorizedRequestError();
const serviceTokenData = await ServiceTokenDataV3.findOne({
_id: new Types.ObjectId(decodedToken.serviceTokenDataId),
isActive: true
});
if (!serviceTokenData) {
throw UnauthorizedRequestError({
message: "Failed to authenticate"
});
} else if (serviceTokenData?.expiresAt && new Date(serviceTokenData.expiresAt) < new Date()) {
// case: service token expired
await ServiceTokenDataV3.findByIdAndUpdate(
serviceTokenData._id,
{
isActive: false
},
{
new: true
}
);
throw UnauthorizedRequestError({
message: "Failed to authenticate",
});
} else if (decodedToken.tokenVersion !== serviceTokenData.tokenVersion) {
// TODO: raise alarm
throw UnauthorizedRequestError({
message: "Failed to authenticate",
});
}
await ServiceTokenDataV3.findByIdAndUpdate(
serviceTokenData._id,
{
accessTokenLastUsed: new Date(),
$inc: { accessTokenUsageCount: 1 }
},
{
new: true
}
);
return {
actor: {
type: ActorType.SERVICE_V3,
metadata: {
serviceId: serviceTokenData._id.toString(),
name: serviceTokenData.name
}
},
authPayload: serviceTokenData,
ipAddress: req.realIP,
userAgent: req.headers["user-agent"] ?? "",
userAgentType: getUserAgentType(req.headers["user-agent"])
}
}
/**
* Return API key data payload corresponding to API key [authTokenValue]
* @param {Object} obj
* @param {String} obj.authTokenValue - API key value
* @returns {APIKeyData} apiKeyData - API key data
*/
export const getAuthAPIKeyPayload = async ({
req,
authTokenValue,
}: {
req: Request,
authTokenValue: string;
}): Promise<UserAuthData> => {
const [_, TOKEN_IDENTIFIER, TOKEN_SECRET] = <[string, string, string]>authTokenValue.split(".", 3);
let apiKeyData = await APIKeyData
.findById(TOKEN_IDENTIFIER, "+secretHash +expiresAt")
.populate<{ user: IUser }>("user", "+publicKey");
if (!apiKeyData) {
throw APIKeyDataNotFoundError({ message: "Failed to find API key data" });
} else if (apiKeyData?.expiresAt && new Date(apiKeyData.expiresAt) < new Date()) {
// case: API key expired
await APIKeyData.findByIdAndDelete(apiKeyData._id);
throw UnauthorizedRequestError({
message: "Failed to authenticate expired API key",
});
}
const isMatch = await bcrypt.compare(TOKEN_SECRET, apiKeyData.secretHash);
if (!isMatch) throw UnauthorizedRequestError({
message: "Failed to authenticate API key",
});
apiKeyData = await APIKeyData.findOneAndUpdate({
_id: new Types.ObjectId(TOKEN_IDENTIFIER),
}, {
lastUsed: new Date(),
}, {
new: true,
});
if (!apiKeyData) {
throw APIKeyDataNotFoundError({ message: "Failed to find API key data" });
}
const user = await User.findById(apiKeyData.user).select("+publicKey");
if (!user) {
throw AccountNotFoundError({
message: "Failed to find user",
});
}
return {
actor: {
type: ActorType.USER,
metadata: {
userId: user._id.toString(),
email: user.email
}
},
authPayload: user,
ipAddress: req.realIP,
userAgent: req.headers["user-agent"] ?? "",
userAgentType: getUserAgentType(req.headers["user-agent"])
}
}
import { AuthTokenType } from "../variables";
/**
* Return newly issued (JWT) auth and refresh tokens to user with id [userId]

View File

@@ -49,7 +49,7 @@ import {
import { TelemetryService } from "../services";
import { client, getEncryptionKey, getRootEncryptionKey } from "../config";
import { EEAuditLogService, EELogService, EESecretService } from "../ee/services";
import { getAuthDataPayloadIdObj, getAuthDataPayloadUserObj } from "../utils/auth/authDataExtractors";
import { getAuthDataPayloadIdObj, getAuthDataPayloadUserObj } from "../utils/authn/authDataExtractors";
import { getFolderByPath, getFolderIdFromServiceToken } from "../services/FolderService";
import picomatch from "picomatch";
import path from "path";

View File

@@ -1,14 +1,9 @@
import jwt from "jsonwebtoken";
import { NextFunction, Request, Response } from "express";
import {
getAuthAPIKeyPayload,
getAuthSTDPayload,
getAuthSTDV3Payload,
getAuthUserPayload,
validateAuthMode,
} from "../helpers/auth";
import { AuthMode } from "../variables";
import { AuthData } from "../interfaces/middleware";
import { extractAuthMode, getAuthData } from "../utils/authn/authMode";
import { UnauthorizedRequestError } from "../utils/errors";
declare module "jsonwebtoken" {
export interface UserIDJwtPayload extends jwt.JwtPayload {
@@ -32,42 +27,39 @@ const requireAuth = ({
acceptedAuthModes: AuthMode[];
}) => {
return async (req: Request, res: Response, next: NextFunction) => {
// extract auth mode
const { authMode, authTokenValue } = await extractAuthMode({
headers: req.headers
});
// validate auth token against accepted auth modes [acceptedAuthModes]
// and return token type [authTokenType] and value [authTokenValue]
const { authMode, authTokenValue } = await validateAuthMode({
headers: req.headers,
acceptedAuthModes,
// validate auth mode
if (!acceptedAuthModes.includes(authMode)) throw UnauthorizedRequestError({
message: "Failed to authenticate unaccepted authentication mode"
});
let authData: AuthData;
// get auth data / payload
const authData: AuthData = await getAuthData({
authMode,
authTokenValue,
ipAddress: req.realIP,
userAgent: req.headers["user-agent"] ?? ""
});
switch (authMode) {
case AuthMode.SERVICE_TOKEN:
authData = await getAuthSTDPayload({
req,
authTokenValue,
});
req.serviceTokenData = authData.authPayload;
break;
case AuthMode.SERVICE_ACCESS_TOKEN:
authData = await getAuthSTDV3Payload({
req,
authTokenValue
});
req.serviceTokenData = authData.authPayload;
break;
case AuthMode.API_KEY:
authData = await getAuthAPIKeyPayload({
req,
authTokenValue
});
req.user = authData.authPayload;
break;
case AuthMode.API_KEY_V2:
req.user = authData.authPayload;
break;
case AuthMode.JWT:
authData = await getAuthUserPayload({
req,
authTokenValue
});
req.user = authData.authPayload;
break;
}

View File

@@ -7,7 +7,7 @@ import { AuthMode } from "../../variables";
router.get(
"/raw",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_ACCESS_TOKEN]
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.API_KEY_V2, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_ACCESS_TOKEN]
}),
secretsController.getSecretsRaw
);
@@ -15,7 +15,7 @@ router.get(
router.get(
"/raw/:secretName",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_ACCESS_TOKEN]
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.API_KEY_V2, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_ACCESS_TOKEN]
}),
requireBlindIndicesEnabled({
locationWorkspaceId: "query"
@@ -29,7 +29,7 @@ router.get(
router.post(
"/raw/:secretName",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_ACCESS_TOKEN]
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.API_KEY_V2, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_ACCESS_TOKEN]
}),
requireBlindIndicesEnabled({
locationWorkspaceId: "body"
@@ -43,7 +43,7 @@ router.post(
router.patch(
"/raw/:secretName",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_ACCESS_TOKEN]
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.API_KEY_V2, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_ACCESS_TOKEN]
}),
requireBlindIndicesEnabled({
locationWorkspaceId: "body"
@@ -57,7 +57,7 @@ router.patch(
router.delete(
"/raw/:secretName",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_ACCESS_TOKEN]
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.API_KEY_V2, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_ACCESS_TOKEN]
}),
requireBlindIndicesEnabled({
locationWorkspaceId: "body"
@@ -71,7 +71,7 @@ router.delete(
router.get(
"/",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_ACCESS_TOKEN]
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.API_KEY_V2, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_ACCESS_TOKEN]
}),
requireBlindIndicesEnabled({
locationWorkspaceId: "query"
@@ -83,7 +83,7 @@ router.get(
router.post(
"/batch",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.API_KEY_V2, AuthMode.SERVICE_TOKEN]
}),
requireBlindIndicesEnabled({
locationWorkspaceId: "body"
@@ -94,7 +94,7 @@ router.post(
router.patch(
"/batch",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.API_KEY_V2, AuthMode.SERVICE_TOKEN]
}),
requireBlindIndicesEnabled({
locationWorkspaceId: "body"
@@ -105,7 +105,7 @@ router.patch(
router.delete(
"/batch",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.API_KEY_V2, AuthMode.SERVICE_TOKEN]
}),
requireBlindIndicesEnabled({
locationWorkspaceId: "body"
@@ -116,7 +116,7 @@ router.delete(
router.post(
"/:secretName",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_ACCESS_TOKEN]
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.API_KEY_V2, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_ACCESS_TOKEN]
}),
requireBlindIndicesEnabled({
locationWorkspaceId: "body"
@@ -127,7 +127,7 @@ router.post(
router.get(
"/:secretName",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_ACCESS_TOKEN]
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.API_KEY_V2, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_ACCESS_TOKEN]
}),
requireBlindIndicesEnabled({
locationWorkspaceId: "query"
@@ -138,7 +138,7 @@ router.get(
router.patch(
"/:secretName",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_ACCESS_TOKEN]
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.API_KEY_V2, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_ACCESS_TOKEN]
}),
requireBlindIndicesEnabled({
locationWorkspaceId: "body"
@@ -149,7 +149,7 @@ router.patch(
router.delete(
"/:secretName",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_ACCESS_TOKEN]
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.API_KEY_V2, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_ACCESS_TOKEN]
}),
requireBlindIndicesEnabled({
locationWorkspaceId: "body"

View File

@@ -0,0 +1,50 @@
import { Types } from "mongoose";
import {
APIKeyData,
IUser,
User
} from "../../../models";
import { AccountNotFoundError, UnauthorizedRequestError } from "../../errors";
import bcrypt from "bcrypt";
interface ValidateAPIKeyParams {
authTokenValue: string;
}
export const validateAPIKey = async ({
authTokenValue
}: ValidateAPIKeyParams) => {
const [_, TOKEN_IDENTIFIER, TOKEN_SECRET] = <[string, string, string]>authTokenValue.split(".", 3);
let apiKeyData = await APIKeyData
.findById(TOKEN_IDENTIFIER, "+secretHash +expiresAt")
.populate<{ user: IUser }>("user", "+publicKey");
if (!apiKeyData) {
throw UnauthorizedRequestError();
} else if (apiKeyData?.expiresAt && new Date(apiKeyData.expiresAt) < new Date()) {
// case: API key expired
await APIKeyData.findByIdAndDelete(apiKeyData._id);
throw UnauthorizedRequestError();
}
const isMatch = await bcrypt.compare(TOKEN_SECRET, apiKeyData.secretHash);
if (!isMatch) throw UnauthorizedRequestError();
apiKeyData = await APIKeyData.findOneAndUpdate({
_id: new Types.ObjectId(TOKEN_IDENTIFIER),
}, {
lastUsed: new Date(),
}, {
new: true,
});
if (!apiKeyData) throw UnauthorizedRequestError();
const user = await User.findById(apiKeyData.user).select("+publicKey");
if (!user) throw AccountNotFoundError();
return user;
}

View File

@@ -0,0 +1,39 @@
import jwt from "jsonwebtoken";
import { APIKeyDataV2, User } from "../../../models";
import { getAuthSecret } from "../../../config";
import { AuthTokenType } from "../../../variables";
import { AccountNotFoundError, UnauthorizedRequestError } from "../../errors";
interface ValidateAPIKeyV2Params {
authTokenValue: string;
}
export const validateAPIKeyV2 = async ({
authTokenValue
}: ValidateAPIKeyV2Params) => {
const decodedToken = <jwt.UserIDJwtPayload>(
jwt.verify(authTokenValue, await getAuthSecret())
);
if (decodedToken.authTokenType !== AuthTokenType.API_KEY) throw UnauthorizedRequestError();
const apiKeyData = await APIKeyDataV2.findByIdAndUpdate(
decodedToken.apiKeyDataId,
{
lastUsed: new Date(),
$inc: { usageCount: 1 }
},
{
new: true
}
);
if (!apiKeyData) throw UnauthorizedRequestError();
const user = await User.findById(apiKeyData.user).select("+publicKey");
if (!user) throw AccountNotFoundError();
return user;
}

View File

@@ -0,0 +1,191 @@
import jwt from "jsonwebtoken";
import { getAuthSecret } from "../../../config";
import { ActorType } from "../../../ee/models";
import { AuthMode, AuthTokenType } from "../../../variables";
import { UnauthorizedRequestError } from "../../errors";
import { validateAPIKey } from "./apiKey";
import { validateAPIKeyV2 } from "./apiKeyV2";
import { validateServiceTokenV2 } from "./serviceTokenV2";
import { validateServiceTokenV3 } from "./serviceTokenV3";
import { validateJWT } from "./jwt";
import { getUserAgentType } from "../../posthog";
import { AuthData } from "../../../interfaces/middleware";
interface ExtractAuthModeParams {
headers: { [key: string]: string | string[] | undefined }
}
interface ExtractAuthModeReturn {
authMode: AuthMode;
authTokenValue: string;
}
interface GetAuthDataParams {
authMode: AuthMode;
authTokenValue: string;
ipAddress: string;
userAgent: string;
}
/**
* Returns the recognized authentication mode based on token in [headers]; accepted token types include:
* - SERVICE_TOKEN
* - API_KEY
* - JWT
* - SERVICE_ACCESS_TOKEN (from ST V3)
* - API_KEY_V2
* @param {Object} params
* @param {Object.<string, (string|string[]|undefined)>} params.headers - The HTTP request headers, usually from Express's `req.headers`.
* @returns {Promise<AuthMode>} The derived authentication mode based on the headers.
* @throws {UnauthorizedError} Throws an error if no applicable authMode is found.
*/
export const extractAuthMode = async ({
headers
}: ExtractAuthModeParams): Promise<ExtractAuthModeReturn> => {
const apiKey = headers["x-api-key"] as string;
const authHeader = headers["authorization"] as string;
if (apiKey) {
return { authMode: AuthMode.API_KEY, authTokenValue: apiKey };
}
if (!authHeader) throw UnauthorizedRequestError({
message: "Failed to authenticate unknown authentication method"
});
if (!authHeader.startsWith("Bearer ")) throw UnauthorizedRequestError({
message: "Failed to authenticate unknown authentication method"
});
const authTokenValue = authHeader.slice(7);
if (authTokenValue.startsWith("st.")) {
return { authMode: AuthMode.SERVICE_TOKEN, authTokenValue };
}
const decodedToken = <jwt.AuthnJwtPayload>(
jwt.verify(authTokenValue, await getAuthSecret())
);
switch (decodedToken.authTokenType) {
case AuthTokenType.ACCESS_TOKEN:
return { authMode: AuthMode.JWT, authTokenValue };
case AuthTokenType.API_KEY:
return { authMode: AuthMode.API_KEY_V2, authTokenValue };
case AuthTokenType.SERVICE_ACCESS_TOKEN:
return { authMode: AuthMode.SERVICE_ACCESS_TOKEN, authTokenValue };
default:
throw UnauthorizedRequestError({
message: "Failed to authenticate unknown authentication method"
});
}
}
export const getAuthData = async ({
authMode,
authTokenValue,
ipAddress,
userAgent
}: GetAuthDataParams): Promise<AuthData> => {
const userAgentType = getUserAgentType(userAgent);
switch (authMode) {
case AuthMode.SERVICE_TOKEN: {
const serviceTokenData = await validateServiceTokenV2({
authTokenValue
});
return {
actor: {
type: ActorType.SERVICE,
metadata: {
serviceId: serviceTokenData._id.toString(),
name: serviceTokenData.name
}
},
authPayload: serviceTokenData,
ipAddress,
userAgent,
userAgentType
}
}
case AuthMode.SERVICE_ACCESS_TOKEN: {
const serviceTokenData = await validateServiceTokenV3({
authTokenValue
});
return {
actor: {
type: ActorType.SERVICE_V3,
metadata: {
serviceId: serviceTokenData._id.toString(),
name: serviceTokenData.name
}
},
authPayload: serviceTokenData,
ipAddress,
userAgent,
userAgentType
}
}
case AuthMode.API_KEY: {
const user = await validateAPIKey({
authTokenValue
});
return {
actor: {
type: ActorType.USER,
metadata: {
userId: user._id.toString(),
email: user.email
}
},
authPayload: user,
ipAddress,
userAgent,
userAgentType
}
}
case AuthMode.API_KEY_V2: {
const user = await validateAPIKeyV2({
authTokenValue
});
return {
actor: {
type: ActorType.USER,
metadata: {
userId: user._id.toString(),
email: user.email
}
},
authPayload: user,
ipAddress,
userAgent,
userAgentType
}
}
case AuthMode.JWT: {
const user = await validateJWT({
authTokenValue
});
return {
actor: {
type: ActorType.USER,
metadata: {
userId: user._id.toString(),
email: user.email
}
},
authPayload: user,
ipAddress,
userAgent,
userAgentType
}
}
}
}

View File

@@ -0,0 +1,4 @@
export {
extractAuthMode,
getAuthData
} from "./helpers";

View File

@@ -0,0 +1,41 @@
import jwt from "jsonwebtoken";
import { Types } from "mongoose";
import { TokenVersion, User } from "../../../models";
import { getAuthSecret } from "../../../config";
import { AuthTokenType } from "../../../variables";
import { AccountNotFoundError, UnauthorizedRequestError } from "../../errors";
interface ValidateJWTParams {
authTokenValue: string;
}
export const validateJWT = async ({
authTokenValue
}: ValidateJWTParams) => {
const decodedToken = <jwt.UserIDJwtPayload>(
jwt.verify(authTokenValue, await getAuthSecret())
);
if (decodedToken.authTokenType !== AuthTokenType.ACCESS_TOKEN) throw UnauthorizedRequestError();
const tokenVersion = await TokenVersion.findOneAndUpdate({
_id: new Types.ObjectId(decodedToken.tokenVersionId),
user: decodedToken.userId
}, {
lastUsed: new Date(),
});
if (!tokenVersion) throw UnauthorizedRequestError();
if (decodedToken.accessVersion !== tokenVersion.accessVersion) throw UnauthorizedRequestError();
const user = await User.findOne({
_id: new Types.ObjectId(decodedToken.userId),
}).select("+publicKey");
if (!user) throw AccountNotFoundError({ message: "Failed to find user" });
if (!user?.publicKey) throw UnauthorizedRequestError({ message: "Failed to authenticate user with partially set up account" });
return user;
}

View File

@@ -0,0 +1,44 @@
import { Types } from "mongoose";
import { ServiceTokenData } from "../../../models";
import { ResourceNotFoundError, UnauthorizedRequestError } from "../../errors";
import bcrypt from "bcrypt";
interface ValidateServiceTokenV2Params {
authTokenValue: string;
}
export const validateServiceTokenV2 = async ({
authTokenValue
}: ValidateServiceTokenV2Params) => {
const [_, TOKEN_IDENTIFIER, TOKEN_SECRET] = <[string, string, string]>authTokenValue.split(".", 3);
const serviceTokenData = await ServiceTokenData
.findById(TOKEN_IDENTIFIER, "+secretHash +expiresAt")
if (!serviceTokenData) {
throw UnauthorizedRequestError();
} else if (serviceTokenData?.expiresAt && new Date(serviceTokenData.expiresAt) < new Date()) {
// case: service token expired
await ServiceTokenData.findByIdAndDelete(serviceTokenData._id);
throw UnauthorizedRequestError({
message: "Failed to authenticate expired service token",
});
}
const isMatch = await bcrypt.compare(TOKEN_SECRET, serviceTokenData.secretHash);
if (!isMatch) throw UnauthorizedRequestError();
const serviceTokenDataToReturn = await ServiceTokenData
.findOneAndUpdate({
_id: new Types.ObjectId(TOKEN_IDENTIFIER),
}, {
lastUsed: new Date(),
}, {
new: true,
})
.select("+encryptedKey +iv +tag")
if (!serviceTokenDataToReturn) throw ResourceNotFoundError();
return serviceTokenDataToReturn;
}

View File

@@ -0,0 +1,64 @@
import jwt from "jsonwebtoken";
import { Types } from "mongoose";
import { ServiceTokenDataV3 } from "../../../models";
import { getAuthSecret } from "../../../config";
import { AuthTokenType } from "../../../variables";
import { UnauthorizedRequestError } from "../../errors";
interface ValidateServiceTokenV3Params {
authTokenValue: string;
}
export const validateServiceTokenV3 = async ({
authTokenValue
}: ValidateServiceTokenV3Params) => {
const decodedToken = <jwt.ServiceRefreshTokenJwtPayload>(
jwt.verify(authTokenValue, await getAuthSecret())
);
if (decodedToken.authTokenType !== AuthTokenType.SERVICE_ACCESS_TOKEN) throw UnauthorizedRequestError();
const serviceTokenData = await ServiceTokenDataV3.findOne({
_id: new Types.ObjectId(decodedToken.serviceTokenDataId),
isActive: true
});
if (!serviceTokenData) {
throw UnauthorizedRequestError({
message: "Failed to authenticate"
});
} else if (serviceTokenData?.expiresAt && new Date(serviceTokenData.expiresAt) < new Date()) {
// case: service token expired
await ServiceTokenDataV3.findByIdAndUpdate(
serviceTokenData._id,
{
isActive: false
},
{
new: true
}
);
throw UnauthorizedRequestError({
message: "Failed to authenticate",
});
} else if (decodedToken.tokenVersion !== serviceTokenData.tokenVersion) {
// TODO: raise alarm
throw UnauthorizedRequestError({
message: "Failed to authenticate",
});
}
await ServiceTokenDataV3.findByIdAndUpdate(
serviceTokenData._id,
{
accessTokenLastUsed: new Date(),
$inc: { accessTokenUsageCount: 1 }
},
{
new: true
}
);
return serviceTokenData;
}

View File

@@ -31,7 +31,7 @@ import {
initializeGitLabStrategy,
initializeGoogleStrategy,
initializeSamlStrategy
} from "../auth/passport";
} from "../authn/passport";
/**
* Prepare Infisical upon startup. This includes tasks like:

View File

@@ -15,7 +15,8 @@ export enum AuthMode {
JWT = "jwt",
SERVICE_TOKEN = "serviceToken",
SERVICE_ACCESS_TOKEN = "serviceAccessToken",
API_KEY = "apiKey"
API_KEY = "apiKey",
API_KEY_V2 = "apiKeyV2"
}
export const K8_USER_AGENT_NAME = "k8-operator"

View File

@@ -1,10 +1,10 @@
// import { APIKeyV2Section } from "../APIKeyV2Section";
import { APIKeySection } from "../APIKeySection";
import { APIKeyV2Section } from "../APIKeyV2Section";
export const PersonalAPIKeyTab = () => {
return (
<>
{/* <APIKeyV2Section /> */}
<APIKeyV2Section />
<APIKeySection />
</>
);

View File

@@ -1,10 +1,10 @@
import { ServiceTokenSection } from "../ServiceTokenSection";
// import { ServiceTokenV3Section } from "../ServiceTokenV3Section";
import { ServiceTokenV3Section } from "../ServiceTokenV3Section";
export const ProjectServiceTokensTab = () => {
return (
<>
{/* <ServiceTokenV3Section /> */}
<ServiceTokenV3Section />
<ServiceTokenSection />
</>
);