mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Finish preliminary refactor/modularization of requireAuth logic
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
50
backend/src/utils/authn/authMode/apiKey.ts
Normal file
50
backend/src/utils/authn/authMode/apiKey.ts
Normal 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;
|
||||
}
|
||||
39
backend/src/utils/authn/authMode/apiKeyV2.ts
Normal file
39
backend/src/utils/authn/authMode/apiKeyV2.ts
Normal 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;
|
||||
}
|
||||
191
backend/src/utils/authn/authMode/helpers.ts
Normal file
191
backend/src/utils/authn/authMode/helpers.ts
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
4
backend/src/utils/authn/authMode/index.ts
Normal file
4
backend/src/utils/authn/authMode/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export {
|
||||
extractAuthMode,
|
||||
getAuthData
|
||||
} from "./helpers";
|
||||
41
backend/src/utils/authn/authMode/jwt.ts
Normal file
41
backend/src/utils/authn/authMode/jwt.ts
Normal 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;
|
||||
}
|
||||
44
backend/src/utils/authn/authMode/serviceTokenV2.ts
Normal file
44
backend/src/utils/authn/authMode/serviceTokenV2.ts
Normal 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;
|
||||
}
|
||||
64
backend/src/utils/authn/authMode/serviceTokenV3.ts
Normal file
64
backend/src/utils/authn/authMode/serviceTokenV3.ts
Normal 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;
|
||||
}
|
||||
@@ -31,7 +31,7 @@ import {
|
||||
initializeGitLabStrategy,
|
||||
initializeGoogleStrategy,
|
||||
initializeSamlStrategy
|
||||
} from "../auth/passport";
|
||||
} from "../authn/passport";
|
||||
|
||||
/**
|
||||
* Prepare Infisical upon startup. This includes tasks like:
|
||||
|
||||
@@ -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"
|
||||
@@ -1,10 +1,10 @@
|
||||
// import { APIKeyV2Section } from "../APIKeyV2Section";
|
||||
import { APIKeySection } from "../APIKeySection";
|
||||
import { APIKeyV2Section } from "../APIKeyV2Section";
|
||||
|
||||
export const PersonalAPIKeyTab = () => {
|
||||
return (
|
||||
<>
|
||||
{/* <APIKeyV2Section /> */}
|
||||
<APIKeyV2Section />
|
||||
<APIKeySection />
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { ServiceTokenSection } from "../ServiceTokenSection";
|
||||
// import { ServiceTokenV3Section } from "../ServiceTokenV3Section";
|
||||
import { ServiceTokenV3Section } from "../ServiceTokenV3Section";
|
||||
|
||||
export const ProjectServiceTokensTab = () => {
|
||||
return (
|
||||
<>
|
||||
{/* <ServiceTokenV3Section /> */}
|
||||
<ServiceTokenV3Section />
|
||||
<ServiceTokenSection />
|
||||
</>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user