mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 21:27:10 +00:00
Merge pull request #1126 from Infisical/stv3-update
Multipart Update to Authentication (ST V3, Modularization of Auth Validation Methods, SSO logic)
This commit is contained in:
@@ -24,7 +24,6 @@ export const getJwtRefreshLifetime = async () => (await client.getSecret("JWT_RE
|
|||||||
export const getJwtServiceSecret = async () => (await client.getSecret("JWT_SERVICE_SECRET")).secretValue; // TODO: deprecate (related to ST V1)
|
export const getJwtServiceSecret = async () => (await client.getSecret("JWT_SERVICE_SECRET")).secretValue; // TODO: deprecate (related to ST V1)
|
||||||
export const getJwtSignupLifetime = async () => (await client.getSecret("JWT_SIGNUP_LIFETIME")).secretValue || "15m";
|
export const getJwtSignupLifetime = async () => (await client.getSecret("JWT_SIGNUP_LIFETIME")).secretValue || "15m";
|
||||||
export const getJwtProviderAuthLifetime = async () => (await client.getSecret("JWT_PROVIDER_AUTH_LIFETIME")).secretValue || "15m";
|
export const getJwtProviderAuthLifetime = async () => (await client.getSecret("JWT_PROVIDER_AUTH_LIFETIME")).secretValue || "15m";
|
||||||
export const getJwtServiceTokenSecret = async () => (await client.getSecret("JWT_SERVICE_TOKEN_SECRET")).secretValue;
|
|
||||||
export const getMongoURL = async () => (await client.getSecret("MONGO_URL")).secretValue;
|
export const getMongoURL = async () => (await client.getSecret("MONGO_URL")).secretValue;
|
||||||
export const getNodeEnv = async () => (await client.getSecret("NODE_ENV")).secretValue || "production";
|
export const getNodeEnv = async () => (await client.getSecret("NODE_ENV")).secretValue || "production";
|
||||||
export const getVerboseErrorOutput = async () => (await client.getSecret("VERBOSE_ERROR_OUTPUT")).secretValue === "true" && true;
|
export const getVerboseErrorOutput = async () => (await client.getSecret("VERBOSE_ERROR_OUTPUT")).secretValue === "true" && true;
|
||||||
|
|||||||
@@ -24,10 +24,18 @@ import { validateRequest } from "../../helpers/validation";
|
|||||||
import * as reqValidator from "../../validation/auth";
|
import * as reqValidator from "../../validation/auth";
|
||||||
|
|
||||||
declare module "jsonwebtoken" {
|
declare module "jsonwebtoken" {
|
||||||
|
export interface AuthnJwtPayload extends jwt.JwtPayload {
|
||||||
|
authTokenType: AuthTokenType;
|
||||||
|
}
|
||||||
export interface UserIDJwtPayload extends jwt.JwtPayload {
|
export interface UserIDJwtPayload extends jwt.JwtPayload {
|
||||||
userId: string;
|
userId: string;
|
||||||
refreshVersion?: number;
|
refreshVersion?: number;
|
||||||
}
|
}
|
||||||
|
export interface ServiceRefreshTokenJwtPayload extends jwt.JwtPayload {
|
||||||
|
serviceTokenDataId: string;
|
||||||
|
authTokenType: string;
|
||||||
|
tokenVersion: number;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import jwt from "jsonwebtoken";
|
||||||
import { Request, Response } from "express";
|
import { Request, Response } from "express";
|
||||||
import { Types } from "mongoose";
|
import { Types } from "mongoose";
|
||||||
import {
|
import {
|
||||||
@@ -24,10 +25,11 @@ import {
|
|||||||
getUserProjectPermissions
|
getUserProjectPermissions
|
||||||
} from "../../services/ProjectRoleService";
|
} from "../../services/ProjectRoleService";
|
||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
import { BadRequestError, ResourceNotFoundError } from "../../../utils/errors";
|
import { BadRequestError, ResourceNotFoundError, UnauthorizedRequestError } from "../../../utils/errors";
|
||||||
import { extractIPDetails, isValidIpOrCidr } from "../../../utils/ip";
|
import { extractIPDetails, isValidIpOrCidr } from "../../../utils/ip";
|
||||||
import { EEAuditLogService, EELicenseService } from "../../services";
|
import { EEAuditLogService, EELicenseService } from "../../services";
|
||||||
import { getJwtServiceTokenSecret } from "../../../config";
|
import { getAuthSecret } from "../../../config";
|
||||||
|
import { AuthTokenType } from "../../../variables";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return project key for service token V3
|
* Return project key for service token V3
|
||||||
@@ -56,6 +58,99 @@ export const getServiceTokenDataKey = async (req: Request, res: Response) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return access and refresh token as per refresh operation
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
*/
|
||||||
|
export const refreshToken = async (req: Request, res: Response) => {
|
||||||
|
const {
|
||||||
|
body: {
|
||||||
|
refresh_token
|
||||||
|
}
|
||||||
|
} = await validateRequest(reqValidator.RefreshTokenV3, req);
|
||||||
|
|
||||||
|
const decodedToken = <jwt.ServiceRefreshTokenJwtPayload>(
|
||||||
|
jwt.verify(refresh_token, await getAuthSecret())
|
||||||
|
);
|
||||||
|
|
||||||
|
if (decodedToken.authTokenType !== AuthTokenType.SERVICE_REFRESH_TOKEN) throw UnauthorizedRequestError();
|
||||||
|
|
||||||
|
let serviceTokenData = await ServiceTokenDataV3.findOne({
|
||||||
|
_id: new Types.ObjectId(decodedToken.serviceTokenDataId),
|
||||||
|
isActive: true
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!serviceTokenData) throw UnauthorizedRequestError();
|
||||||
|
|
||||||
|
if (decodedToken.tokenVersion !== serviceTokenData.tokenVersion) {
|
||||||
|
// raise alarm
|
||||||
|
throw UnauthorizedRequestError();
|
||||||
|
}
|
||||||
|
|
||||||
|
const response: {
|
||||||
|
refresh_token?: string;
|
||||||
|
access_token: string;
|
||||||
|
expires_in: number;
|
||||||
|
token_type: string;
|
||||||
|
} = {
|
||||||
|
refresh_token,
|
||||||
|
access_token: "",
|
||||||
|
expires_in: 0,
|
||||||
|
token_type: "Bearer"
|
||||||
|
};
|
||||||
|
|
||||||
|
if (serviceTokenData.isRefreshTokenRotationEnabled) {
|
||||||
|
serviceTokenData = await ServiceTokenDataV3.findByIdAndUpdate(
|
||||||
|
serviceTokenData._id,
|
||||||
|
{
|
||||||
|
$inc: {
|
||||||
|
tokenVersion: 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
new: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!serviceTokenData) throw BadRequestError();
|
||||||
|
|
||||||
|
response.refresh_token = createToken({
|
||||||
|
payload: {
|
||||||
|
serviceTokenDataId: serviceTokenData._id.toString(),
|
||||||
|
authTokenType: AuthTokenType.SERVICE_REFRESH_TOKEN,
|
||||||
|
tokenVersion: serviceTokenData.tokenVersion
|
||||||
|
},
|
||||||
|
secret: await getAuthSecret()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
response.access_token = createToken({
|
||||||
|
payload: {
|
||||||
|
serviceTokenDataId: serviceTokenData._id.toString(),
|
||||||
|
authTokenType: AuthTokenType.SERVICE_ACCESS_TOKEN,
|
||||||
|
tokenVersion: serviceTokenData.tokenVersion
|
||||||
|
},
|
||||||
|
expiresIn: serviceTokenData.accessTokenTTL,
|
||||||
|
secret: await getAuthSecret()
|
||||||
|
});
|
||||||
|
|
||||||
|
response.expires_in = serviceTokenData.accessTokenTTL;
|
||||||
|
|
||||||
|
await ServiceTokenDataV3.findByIdAndUpdate(
|
||||||
|
serviceTokenData._id,
|
||||||
|
{
|
||||||
|
refreshTokenLastUsed: new Date(),
|
||||||
|
$inc: { refreshTokenUsageCount: 1 }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
new: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return res.status(200).send(response);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create service token data V3
|
* Create service token data V3
|
||||||
* @param req
|
* @param req
|
||||||
@@ -71,8 +166,10 @@ export const createServiceTokenData = async (req: Request, res: Response) => {
|
|||||||
scopes,
|
scopes,
|
||||||
trustedIps,
|
trustedIps,
|
||||||
expiresIn,
|
expiresIn,
|
||||||
|
accessTokenTTL,
|
||||||
|
isRefreshTokenRotationEnabled,
|
||||||
encryptedKey, // for ServiceTokenDataV3Key
|
encryptedKey, // for ServiceTokenDataV3Key
|
||||||
nonce // for ServiceTokenDataV3Key
|
nonce, // for ServiceTokenDataV3Key
|
||||||
}
|
}
|
||||||
} = await validateRequest(reqValidator.CreateServiceTokenV3, req);
|
} = await validateRequest(reqValidator.CreateServiceTokenV3, req);
|
||||||
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
|
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
|
||||||
@@ -118,11 +215,15 @@ export const createServiceTokenData = async (req: Request, res: Response) => {
|
|||||||
user,
|
user,
|
||||||
workspace: new Types.ObjectId(workspaceId),
|
workspace: new Types.ObjectId(workspaceId),
|
||||||
publicKey,
|
publicKey,
|
||||||
usageCount: 0,
|
refreshTokenUsageCount: 0,
|
||||||
|
accessTokenUsageCount: 0,
|
||||||
|
tokenVersion: 1,
|
||||||
trustedIps: reformattedTrustedIps,
|
trustedIps: reformattedTrustedIps,
|
||||||
scopes,
|
scopes,
|
||||||
isActive,
|
isActive,
|
||||||
expiresAt
|
expiresAt,
|
||||||
|
accessTokenTTL,
|
||||||
|
isRefreshTokenRotationEnabled
|
||||||
}).save();
|
}).save();
|
||||||
|
|
||||||
await new ServiceTokenDataV3Key({
|
await new ServiceTokenDataV3Key({
|
||||||
@@ -133,18 +234,19 @@ export const createServiceTokenData = async (req: Request, res: Response) => {
|
|||||||
workspace: new Types.ObjectId(workspaceId)
|
workspace: new Types.ObjectId(workspaceId)
|
||||||
}).save();
|
}).save();
|
||||||
|
|
||||||
const token = createToken({
|
const refreshToken = createToken({
|
||||||
payload: {
|
payload: {
|
||||||
_id: serviceTokenData._id.toString()
|
serviceTokenDataId: serviceTokenData._id.toString(),
|
||||||
|
authTokenType: AuthTokenType.SERVICE_REFRESH_TOKEN,
|
||||||
|
tokenVersion: serviceTokenData.tokenVersion
|
||||||
},
|
},
|
||||||
expiresIn,
|
secret: await getAuthSecret()
|
||||||
secret: await getJwtServiceTokenSecret()
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await EEAuditLogService.createAuditLog(
|
await EEAuditLogService.createAuditLog(
|
||||||
req.authData,
|
req.authData,
|
||||||
{
|
{
|
||||||
type: EventType.CREATE_SERVICE_TOKEN_V3,
|
type: EventType.CREATE_SERVICE_TOKEN_V3, // TODO: update
|
||||||
metadata: {
|
metadata: {
|
||||||
name,
|
name,
|
||||||
isActive,
|
isActive,
|
||||||
@@ -160,7 +262,7 @@ export const createServiceTokenData = async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
serviceTokenData,
|
serviceTokenData,
|
||||||
serviceToken: `stv3.${token}`
|
refreshToken
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,7 +280,9 @@ export const updateServiceTokenData = async (req: Request, res: Response) => {
|
|||||||
isActive,
|
isActive,
|
||||||
scopes,
|
scopes,
|
||||||
trustedIps,
|
trustedIps,
|
||||||
expiresIn
|
expiresIn,
|
||||||
|
accessTokenTTL,
|
||||||
|
isRefreshTokenRotationEnabled
|
||||||
}
|
}
|
||||||
} = await validateRequest(reqValidator.UpdateServiceTokenV3, req);
|
} = await validateRequest(reqValidator.UpdateServiceTokenV3, req);
|
||||||
|
|
||||||
@@ -233,13 +337,15 @@ export const updateServiceTokenData = async (req: Request, res: Response) => {
|
|||||||
isActive,
|
isActive,
|
||||||
scopes,
|
scopes,
|
||||||
trustedIps: reformattedTrustedIps,
|
trustedIps: reformattedTrustedIps,
|
||||||
expiresAt
|
expiresAt,
|
||||||
|
accessTokenTTL,
|
||||||
|
isRefreshTokenRotationEnabled
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
new: true
|
new: true
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!serviceTokenData) throw BadRequestError({
|
if (!serviceTokenData) throw BadRequestError({
|
||||||
message: "Failed to update service token"
|
message: "Failed to update service token"
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,11 +7,16 @@ import { serviceTokenDataController } from "../../controllers/v3";
|
|||||||
router.get(
|
router.get(
|
||||||
"/me/key",
|
"/me/key",
|
||||||
requireAuth({
|
requireAuth({
|
||||||
acceptedAuthModes: [AuthMode.SERVICE_TOKEN_V3]
|
acceptedAuthModes: [AuthMode.SERVICE_ACCESS_TOKEN]
|
||||||
}),
|
}),
|
||||||
serviceTokenDataController.getServiceTokenDataKey
|
serviceTokenDataController.getServiceTokenDataKey
|
||||||
);
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
"/me/token",
|
||||||
|
serviceTokenDataController.refreshToken
|
||||||
|
);
|
||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
"/",
|
"/",
|
||||||
requireAuth({
|
requireAuth({
|
||||||
|
|||||||
+4
-372
@@ -1,381 +1,13 @@
|
|||||||
import { Request } from "express";
|
|
||||||
import { Types } from "mongoose";
|
import { Types } from "mongoose";
|
||||||
import jwt from "jsonwebtoken";
|
import jwt from "jsonwebtoken";
|
||||||
import bcrypt from "bcrypt";
|
import { ITokenVersion, TokenVersion } from "../models";
|
||||||
import {
|
import { UnauthorizedRequestError } from "../utils/errors";
|
||||||
APIKeyData,
|
|
||||||
APIKeyDataV2,
|
|
||||||
ITokenVersion,
|
|
||||||
IUser,
|
|
||||||
ServiceTokenData,
|
|
||||||
ServiceTokenDataV3,
|
|
||||||
TokenVersion,
|
|
||||||
User,
|
|
||||||
} from "../models";
|
|
||||||
import {
|
|
||||||
APIKeyDataNotFoundError,
|
|
||||||
AccountNotFoundError,
|
|
||||||
BadRequestError,
|
|
||||||
ServiceTokenDataNotFoundError,
|
|
||||||
UnauthorizedRequestError,
|
|
||||||
} from "../utils/errors";
|
|
||||||
import {
|
import {
|
||||||
getAuthSecret,
|
getAuthSecret,
|
||||||
getJwtAuthLifetime,
|
getJwtAuthLifetime,
|
||||||
getJwtRefreshLifetime,
|
getJwtRefreshLifetime
|
||||||
getJwtServiceTokenSecret
|
|
||||||
} from "../config";
|
} from "../config";
|
||||||
import {
|
import { AuthTokenType } from "../variables";
|
||||||
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 = ({
|
|
||||||
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;
|
|
||||||
case "stv3":
|
|
||||||
authMode = AuthMode.SERVICE_TOKEN_V3;
|
|
||||||
authTokenValue = parts.slice(1).join(".");
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
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.UserIDJwtPayload>(
|
|
||||||
jwt.verify(authTokenValue, await getJwtServiceTokenSecret())
|
|
||||||
);
|
|
||||||
|
|
||||||
const serviceTokenData = await ServiceTokenDataV3.findOneAndUpdate(
|
|
||||||
{
|
|
||||||
_id: new Types.ObjectId(decodedToken._id),
|
|
||||||
isActive: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
lastUsed: new Date(),
|
|
||||||
$inc: { usageCount: 1 }
|
|
||||||
},
|
|
||||||
{
|
|
||||||
new: 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",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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"])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return newly issued (JWT) auth and refresh tokens to user with id [userId]
|
* Return newly issued (JWT) auth and refresh tokens to user with id [userId]
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ import {
|
|||||||
import { TelemetryService } from "../services";
|
import { TelemetryService } from "../services";
|
||||||
import { client, getEncryptionKey, getRootEncryptionKey } from "../config";
|
import { client, getEncryptionKey, getRootEncryptionKey } from "../config";
|
||||||
import { EEAuditLogService, EELogService, EESecretService } from "../ee/services";
|
import { EEAuditLogService, EELogService, EESecretService } from "../ee/services";
|
||||||
import { getAuthDataPayloadIdObj, getAuthDataPayloadUserObj } from "../utils/auth";
|
import { getAuthDataPayloadIdObj, getAuthDataPayloadUserObj } from "../utils/authn/helpers";
|
||||||
import { getFolderByPath, getFolderIdFromServiceToken } from "../services/FolderService";
|
import { getFolderByPath, getFolderIdFromServiceToken } from "../services/FolderService";
|
||||||
import picomatch from "picomatch";
|
import picomatch from "picomatch";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
|||||||
@@ -1,14 +1,9 @@
|
|||||||
import jwt from "jsonwebtoken";
|
import jwt from "jsonwebtoken";
|
||||||
import { NextFunction, Request, Response } from "express";
|
import { NextFunction, Request, Response } from "express";
|
||||||
import {
|
|
||||||
getAuthAPIKeyPayload,
|
|
||||||
getAuthSTDPayload,
|
|
||||||
getAuthSTDV3Payload,
|
|
||||||
getAuthUserPayload,
|
|
||||||
validateAuthMode,
|
|
||||||
} from "../helpers/auth";
|
|
||||||
import { AuthMode } from "../variables";
|
import { AuthMode } from "../variables";
|
||||||
import { AuthData } from "../interfaces/middleware";
|
import { AuthData } from "../interfaces/middleware";
|
||||||
|
import { extractAuthMode, getAuthData } from "../utils/authn/helpers";
|
||||||
|
import { UnauthorizedRequestError } from "../utils/errors";
|
||||||
|
|
||||||
declare module "jsonwebtoken" {
|
declare module "jsonwebtoken" {
|
||||||
export interface UserIDJwtPayload extends jwt.JwtPayload {
|
export interface UserIDJwtPayload extends jwt.JwtPayload {
|
||||||
@@ -32,42 +27,39 @@ const requireAuth = ({
|
|||||||
acceptedAuthModes: AuthMode[];
|
acceptedAuthModes: AuthMode[];
|
||||||
}) => {
|
}) => {
|
||||||
return async (req: Request, res: Response, next: NextFunction) => {
|
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]
|
// validate auth mode
|
||||||
// and return token type [authTokenType] and value [authTokenValue]
|
if (!acceptedAuthModes.includes(authMode)) throw UnauthorizedRequestError({
|
||||||
const { authMode, authTokenValue } = validateAuthMode({
|
message: "Failed to authenticate unaccepted authentication mode"
|
||||||
headers: req.headers,
|
|
||||||
acceptedAuthModes,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let authData: AuthData;
|
// get auth data / payload
|
||||||
|
const authData: AuthData = await getAuthData({
|
||||||
|
authMode,
|
||||||
|
authTokenValue,
|
||||||
|
ipAddress: req.realIP,
|
||||||
|
userAgent: req.headers["user-agent"] ?? ""
|
||||||
|
});
|
||||||
|
|
||||||
switch (authMode) {
|
switch (authMode) {
|
||||||
case AuthMode.SERVICE_TOKEN:
|
case AuthMode.SERVICE_TOKEN:
|
||||||
authData = await getAuthSTDPayload({
|
|
||||||
req,
|
|
||||||
authTokenValue,
|
|
||||||
});
|
|
||||||
req.serviceTokenData = authData.authPayload;
|
req.serviceTokenData = authData.authPayload;
|
||||||
break;
|
break;
|
||||||
case AuthMode.SERVICE_TOKEN_V3:
|
case AuthMode.SERVICE_ACCESS_TOKEN:
|
||||||
authData = await getAuthSTDV3Payload({
|
req.serviceTokenData = authData.authPayload;
|
||||||
req,
|
|
||||||
authTokenValue
|
|
||||||
});
|
|
||||||
break;
|
break;
|
||||||
case AuthMode.API_KEY:
|
case AuthMode.API_KEY:
|
||||||
authData = await getAuthAPIKeyPayload({
|
req.user = authData.authPayload;
|
||||||
req,
|
break;
|
||||||
authTokenValue
|
case AuthMode.API_KEY_V2:
|
||||||
});
|
|
||||||
req.user = authData.authPayload;
|
req.user = authData.authPayload;
|
||||||
break;
|
break;
|
||||||
case AuthMode.JWT:
|
case AuthMode.JWT:
|
||||||
authData = await getAuthUserPayload({
|
|
||||||
req,
|
|
||||||
authTokenValue
|
|
||||||
});
|
|
||||||
req.user = authData.authPayload;
|
req.user = authData.authPayload;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,9 +25,14 @@ export interface IServiceTokenDataV3 extends Document {
|
|||||||
user: Types.ObjectId;
|
user: Types.ObjectId;
|
||||||
publicKey: string;
|
publicKey: string;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
lastUsed?: Date;
|
refreshTokenLastUsed?: Date;
|
||||||
usageCount: number;
|
accessTokenLastUsed?: Date;
|
||||||
|
refreshTokenUsageCount: number;
|
||||||
|
accessTokenUsageCount: number;
|
||||||
|
tokenVersion: number;
|
||||||
|
isRefreshTokenRotationEnabled: boolean;
|
||||||
expiresAt?: Date;
|
expiresAt?: Date;
|
||||||
|
accessTokenTTL: number;
|
||||||
scopes: Array<IServiceTokenV3Scope>;
|
scopes: Array<IServiceTokenV3Scope>;
|
||||||
trustedIps: Array<IServiceTokenV3TrustedIp>;
|
trustedIps: Array<IServiceTokenV3TrustedIp>;
|
||||||
}
|
}
|
||||||
@@ -57,19 +62,43 @@ const serviceTokenDataV3Schema = new Schema(
|
|||||||
default: true,
|
default: true,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
lastUsed: {
|
refreshTokenLastUsed: {
|
||||||
type: Date,
|
type: Date,
|
||||||
required: false
|
required: false
|
||||||
},
|
},
|
||||||
usageCount: {
|
accessTokenLastUsed: {
|
||||||
|
type: Date,
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
refreshTokenUsageCount: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 0,
|
default: 0,
|
||||||
required: true
|
required: true
|
||||||
},
|
},
|
||||||
expiresAt: {
|
accessTokenUsageCount: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
tokenVersion: {
|
||||||
|
type: Number,
|
||||||
|
default: 1,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
isRefreshTokenRotationEnabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
expiresAt: { // consider revising field name
|
||||||
type: Date,
|
type: Date,
|
||||||
required: false,
|
required: false,
|
||||||
expires: 0
|
// expires: 0
|
||||||
|
},
|
||||||
|
accessTokenTTL: { // seconds
|
||||||
|
type: Number,
|
||||||
|
default: 7200,
|
||||||
|
required: true
|
||||||
},
|
},
|
||||||
scopes: {
|
scopes: {
|
||||||
type: [
|
type: [
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { AuthMode } from "../../variables";
|
|||||||
router.get(
|
router.get(
|
||||||
"/raw",
|
"/raw",
|
||||||
requireAuth({
|
requireAuth({
|
||||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3]
|
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.API_KEY_V2, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_ACCESS_TOKEN]
|
||||||
}),
|
}),
|
||||||
secretsController.getSecretsRaw
|
secretsController.getSecretsRaw
|
||||||
);
|
);
|
||||||
@@ -15,7 +15,7 @@ router.get(
|
|||||||
router.get(
|
router.get(
|
||||||
"/raw/:secretName",
|
"/raw/:secretName",
|
||||||
requireAuth({
|
requireAuth({
|
||||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3]
|
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.API_KEY_V2, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_ACCESS_TOKEN]
|
||||||
}),
|
}),
|
||||||
requireBlindIndicesEnabled({
|
requireBlindIndicesEnabled({
|
||||||
locationWorkspaceId: "query"
|
locationWorkspaceId: "query"
|
||||||
@@ -29,7 +29,7 @@ router.get(
|
|||||||
router.post(
|
router.post(
|
||||||
"/raw/:secretName",
|
"/raw/:secretName",
|
||||||
requireAuth({
|
requireAuth({
|
||||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3]
|
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.API_KEY_V2, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_ACCESS_TOKEN]
|
||||||
}),
|
}),
|
||||||
requireBlindIndicesEnabled({
|
requireBlindIndicesEnabled({
|
||||||
locationWorkspaceId: "body"
|
locationWorkspaceId: "body"
|
||||||
@@ -43,7 +43,7 @@ router.post(
|
|||||||
router.patch(
|
router.patch(
|
||||||
"/raw/:secretName",
|
"/raw/:secretName",
|
||||||
requireAuth({
|
requireAuth({
|
||||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3]
|
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.API_KEY_V2, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_ACCESS_TOKEN]
|
||||||
}),
|
}),
|
||||||
requireBlindIndicesEnabled({
|
requireBlindIndicesEnabled({
|
||||||
locationWorkspaceId: "body"
|
locationWorkspaceId: "body"
|
||||||
@@ -57,7 +57,7 @@ router.patch(
|
|||||||
router.delete(
|
router.delete(
|
||||||
"/raw/:secretName",
|
"/raw/:secretName",
|
||||||
requireAuth({
|
requireAuth({
|
||||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3]
|
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.API_KEY_V2, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_ACCESS_TOKEN]
|
||||||
}),
|
}),
|
||||||
requireBlindIndicesEnabled({
|
requireBlindIndicesEnabled({
|
||||||
locationWorkspaceId: "body"
|
locationWorkspaceId: "body"
|
||||||
@@ -71,7 +71,7 @@ router.delete(
|
|||||||
router.get(
|
router.get(
|
||||||
"/",
|
"/",
|
||||||
requireAuth({
|
requireAuth({
|
||||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3]
|
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.API_KEY_V2, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_ACCESS_TOKEN]
|
||||||
}),
|
}),
|
||||||
requireBlindIndicesEnabled({
|
requireBlindIndicesEnabled({
|
||||||
locationWorkspaceId: "query"
|
locationWorkspaceId: "query"
|
||||||
@@ -83,7 +83,7 @@ router.get(
|
|||||||
router.post(
|
router.post(
|
||||||
"/batch",
|
"/batch",
|
||||||
requireAuth({
|
requireAuth({
|
||||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]
|
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.API_KEY_V2, AuthMode.SERVICE_TOKEN]
|
||||||
}),
|
}),
|
||||||
requireBlindIndicesEnabled({
|
requireBlindIndicesEnabled({
|
||||||
locationWorkspaceId: "body"
|
locationWorkspaceId: "body"
|
||||||
@@ -94,7 +94,7 @@ router.post(
|
|||||||
router.patch(
|
router.patch(
|
||||||
"/batch",
|
"/batch",
|
||||||
requireAuth({
|
requireAuth({
|
||||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]
|
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.API_KEY_V2, AuthMode.SERVICE_TOKEN]
|
||||||
}),
|
}),
|
||||||
requireBlindIndicesEnabled({
|
requireBlindIndicesEnabled({
|
||||||
locationWorkspaceId: "body"
|
locationWorkspaceId: "body"
|
||||||
@@ -105,7 +105,7 @@ router.patch(
|
|||||||
router.delete(
|
router.delete(
|
||||||
"/batch",
|
"/batch",
|
||||||
requireAuth({
|
requireAuth({
|
||||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]
|
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.API_KEY_V2, AuthMode.SERVICE_TOKEN]
|
||||||
}),
|
}),
|
||||||
requireBlindIndicesEnabled({
|
requireBlindIndicesEnabled({
|
||||||
locationWorkspaceId: "body"
|
locationWorkspaceId: "body"
|
||||||
@@ -116,7 +116,7 @@ router.delete(
|
|||||||
router.post(
|
router.post(
|
||||||
"/:secretName",
|
"/:secretName",
|
||||||
requireAuth({
|
requireAuth({
|
||||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3]
|
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.API_KEY_V2, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_ACCESS_TOKEN]
|
||||||
}),
|
}),
|
||||||
requireBlindIndicesEnabled({
|
requireBlindIndicesEnabled({
|
||||||
locationWorkspaceId: "body"
|
locationWorkspaceId: "body"
|
||||||
@@ -127,7 +127,7 @@ router.post(
|
|||||||
router.get(
|
router.get(
|
||||||
"/:secretName",
|
"/:secretName",
|
||||||
requireAuth({
|
requireAuth({
|
||||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3]
|
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.API_KEY_V2, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_ACCESS_TOKEN]
|
||||||
}),
|
}),
|
||||||
requireBlindIndicesEnabled({
|
requireBlindIndicesEnabled({
|
||||||
locationWorkspaceId: "query"
|
locationWorkspaceId: "query"
|
||||||
@@ -138,7 +138,7 @@ router.get(
|
|||||||
router.patch(
|
router.patch(
|
||||||
"/:secretName",
|
"/:secretName",
|
||||||
requireAuth({
|
requireAuth({
|
||||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3]
|
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.API_KEY_V2, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_ACCESS_TOKEN]
|
||||||
}),
|
}),
|
||||||
requireBlindIndicesEnabled({
|
requireBlindIndicesEnabled({
|
||||||
locationWorkspaceId: "body"
|
locationWorkspaceId: "body"
|
||||||
@@ -149,7 +149,7 @@ router.patch(
|
|||||||
router.delete(
|
router.delete(
|
||||||
"/:secretName",
|
"/:secretName",
|
||||||
requireAuth({
|
requireAuth({
|
||||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_TOKEN_V3]
|
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.API_KEY_V2, AuthMode.SERVICE_TOKEN, AuthMode.SERVICE_ACCESS_TOKEN]
|
||||||
}),
|
}),
|
||||||
requireBlindIndicesEnabled({
|
requireBlindIndicesEnabled({
|
||||||
locationWorkspaceId: "body"
|
locationWorkspaceId: "body"
|
||||||
|
|||||||
@@ -1,435 +0,0 @@
|
|||||||
import express from "express";
|
|
||||||
import passport from "passport";
|
|
||||||
import { Types } from "mongoose";
|
|
||||||
import { AuthData } from "../interfaces/middleware";
|
|
||||||
import {
|
|
||||||
AuthMethod,
|
|
||||||
MembershipOrg,
|
|
||||||
Organization,
|
|
||||||
ServiceAccount,
|
|
||||||
ServiceTokenData,
|
|
||||||
ServiceTokenDataV3,
|
|
||||||
User
|
|
||||||
} from "../models";
|
|
||||||
import { createToken } from "../helpers/auth";
|
|
||||||
import {
|
|
||||||
getAuthSecret,
|
|
||||||
getClientIdGitHubLogin,
|
|
||||||
getClientIdGitLabLogin,
|
|
||||||
getClientIdGoogleLogin,
|
|
||||||
getClientSecretGitHubLogin,
|
|
||||||
getClientSecretGitLabLogin,
|
|
||||||
getClientSecretGoogleLogin,
|
|
||||||
getJwtProviderAuthLifetime,
|
|
||||||
getSiteURL,
|
|
||||||
getUrlGitLabLogin
|
|
||||||
} from "../config";
|
|
||||||
import { getSSOConfigHelper } from "../ee/helpers/organizations";
|
|
||||||
import { InternalServerError, OrganizationNotFoundError } from "./errors";
|
|
||||||
import { ACCEPTED, AuthTokenType, INTEGRATION_GITHUB_API_URL, INVITED, MEMBER } from "../variables";
|
|
||||||
import { standardRequest } from "../config/request";
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
||||||
const GoogleStrategy = require("passport-google-oauth20").Strategy;
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
||||||
const GitHubStrategy = require("passport-github").Strategy;
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
||||||
const GitLabStrategy = require("passport-gitlab2").Strategy;
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
||||||
const { MultiSamlStrategy } = require("@node-saml/passport-saml");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an object containing the id of the authentication data payload
|
|
||||||
* @param {AuthData} authData - authentication data object
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
const getAuthDataPayloadIdObj = (authData: AuthData) => {
|
|
||||||
if (authData.authPayload instanceof User) {
|
|
||||||
return { userId: authData.authPayload._id };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (authData.authPayload instanceof ServiceAccount) {
|
|
||||||
return { serviceAccountId: authData.authPayload._id };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (authData.authPayload instanceof ServiceTokenData) {
|
|
||||||
return { serviceTokenDataId: authData.authPayload._id };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (authData.authPayload instanceof ServiceTokenDataV3) {
|
|
||||||
return { serviceTokenDataId: authData.authPayload._id };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an object containing the user associated with the authentication data payload
|
|
||||||
* @param {AuthData} authData - authentication data object
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
const getAuthDataPayloadUserObj = (authData: AuthData) => {
|
|
||||||
if (authData.authPayload instanceof User) {
|
|
||||||
return { user: authData.authPayload._id };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (authData.authPayload instanceof ServiceAccount) {
|
|
||||||
return { user: authData.authPayload.user };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (authData.authPayload instanceof ServiceTokenData) {
|
|
||||||
return { user: authData.authPayload.user };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (authData.authPayload instanceof ServiceTokenDataV3) {
|
|
||||||
return { user: authData.authPayload.user };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const initializePassport = async () => {
|
|
||||||
const clientIdGoogleLogin = await getClientIdGoogleLogin();
|
|
||||||
const clientSecretGoogleLogin = await getClientSecretGoogleLogin();
|
|
||||||
const clientIdGitHubLogin = await getClientIdGitHubLogin();
|
|
||||||
const clientSecretGitHubLogin = await getClientSecretGitHubLogin();
|
|
||||||
const urlGitLab = await getUrlGitLabLogin();
|
|
||||||
const clientIdGitLabLogin = await getClientIdGitLabLogin();
|
|
||||||
const clientSecretGitLabLogin = await getClientSecretGitLabLogin();
|
|
||||||
|
|
||||||
if (clientIdGoogleLogin && clientSecretGoogleLogin) {
|
|
||||||
passport.use(new GoogleStrategy({
|
|
||||||
passReqToCallback: true,
|
|
||||||
clientID: clientIdGoogleLogin,
|
|
||||||
clientSecret: clientSecretGoogleLogin,
|
|
||||||
callbackURL: "/api/v1/sso/google",
|
|
||||||
scope: ["profile", " email"],
|
|
||||||
}, async (
|
|
||||||
req: express.Request,
|
|
||||||
accessToken: string,
|
|
||||||
refreshToken: string,
|
|
||||||
profile: any,
|
|
||||||
done: any
|
|
||||||
) => {
|
|
||||||
try {
|
|
||||||
const email = profile.emails[0].value;
|
|
||||||
|
|
||||||
let user = await User.findOne({
|
|
||||||
email
|
|
||||||
}).select("+publicKey");
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
user = await new User({
|
|
||||||
email,
|
|
||||||
authMethods: [AuthMethod.GOOGLE],
|
|
||||||
firstName: profile.name.givenName,
|
|
||||||
lastName: profile.name.familyName
|
|
||||||
}).save();
|
|
||||||
}
|
|
||||||
|
|
||||||
let isLinkingRequired = false;
|
|
||||||
if (!user.authMethods.includes(AuthMethod.GOOGLE)) {
|
|
||||||
isLinkingRequired = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isUserCompleted = !!user.publicKey;
|
|
||||||
const providerAuthToken = createToken({
|
|
||||||
payload: {
|
|
||||||
authTokenType: AuthTokenType.PROVIDER_TOKEN,
|
|
||||||
userId: user._id.toString(),
|
|
||||||
email: user.email,
|
|
||||||
firstName: user.firstName,
|
|
||||||
lastName: user.lastName,
|
|
||||||
authMethod: AuthMethod.GOOGLE,
|
|
||||||
isUserCompleted,
|
|
||||||
isLinkingRequired,
|
|
||||||
...(req.query.state ? {
|
|
||||||
callbackPort: req.query.state as string
|
|
||||||
} : {})
|
|
||||||
},
|
|
||||||
expiresIn: await getJwtProviderAuthLifetime(),
|
|
||||||
secret: await getAuthSecret(),
|
|
||||||
});
|
|
||||||
|
|
||||||
req.isUserCompleted = isUserCompleted;
|
|
||||||
req.providerAuthToken = providerAuthToken;
|
|
||||||
done(null, profile);
|
|
||||||
} catch (err) {
|
|
||||||
done(null, false);
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (clientIdGitHubLogin && clientSecretGitHubLogin) {
|
|
||||||
passport.use(new GitHubStrategy({
|
|
||||||
passReqToCallback: true,
|
|
||||||
clientID: clientIdGitHubLogin,
|
|
||||||
clientSecret: clientSecretGitHubLogin,
|
|
||||||
callbackURL: "/api/v1/sso/github",
|
|
||||||
scope: ["user:email"]
|
|
||||||
},
|
|
||||||
async (req : express.Request, accessToken : any, refreshToken : any, profile : any, done : any) => {
|
|
||||||
interface GitHubEmail {
|
|
||||||
email: string;
|
|
||||||
primary: boolean;
|
|
||||||
verified: boolean;
|
|
||||||
visibility: null | string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { data }: { data: GitHubEmail[] } = await standardRequest.get(
|
|
||||||
`${INTEGRATION_GITHUB_API_URL}/user/emails`,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${accessToken}`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const primaryEmail = data.filter((gitHubEmail: GitHubEmail) => gitHubEmail.primary)[0];
|
|
||||||
const email = primaryEmail.email;
|
|
||||||
|
|
||||||
let user = await User.findOne({
|
|
||||||
email
|
|
||||||
}).select("+publicKey");
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
user = await new User({
|
|
||||||
email: email,
|
|
||||||
authMethods: [AuthMethod.GITHUB],
|
|
||||||
firstName: profile.displayName,
|
|
||||||
lastName: ""
|
|
||||||
}).save();
|
|
||||||
}
|
|
||||||
|
|
||||||
let isLinkingRequired = false;
|
|
||||||
if (!user.authMethods.includes(AuthMethod.GITHUB)) {
|
|
||||||
isLinkingRequired = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isUserCompleted = !!user.publicKey;
|
|
||||||
const providerAuthToken = createToken({
|
|
||||||
payload: {
|
|
||||||
authTokenType: AuthTokenType.PROVIDER_TOKEN,
|
|
||||||
userId: user._id.toString(),
|
|
||||||
email: user.email,
|
|
||||||
firstName: user.firstName,
|
|
||||||
lastName: user.lastName,
|
|
||||||
authMethod: AuthMethod.GITHUB,
|
|
||||||
isUserCompleted,
|
|
||||||
isLinkingRequired,
|
|
||||||
...(req.query.state ? {
|
|
||||||
callbackPort: req.query.state as string
|
|
||||||
} : {})
|
|
||||||
},
|
|
||||||
expiresIn: await getJwtProviderAuthLifetime(),
|
|
||||||
secret: await getAuthSecret(),
|
|
||||||
});
|
|
||||||
|
|
||||||
req.isUserCompleted = isUserCompleted;
|
|
||||||
req.providerAuthToken = providerAuthToken;
|
|
||||||
return done(null, profile);
|
|
||||||
}
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (urlGitLab && clientIdGitLabLogin && clientSecretGitLabLogin) {
|
|
||||||
passport.use(new GitLabStrategy({
|
|
||||||
passReqToCallback: true,
|
|
||||||
clientID: clientIdGitLabLogin,
|
|
||||||
clientSecret: clientSecretGitLabLogin,
|
|
||||||
callbackURL: "/api/v1/sso/gitlab",
|
|
||||||
baseURL: urlGitLab
|
|
||||||
},
|
|
||||||
async (req : express.Request, accessToken : any, refreshToken : any, profile : any, done : any) => {
|
|
||||||
const email = profile.emails[0].value;
|
|
||||||
|
|
||||||
let user = await User.findOne({
|
|
||||||
email
|
|
||||||
}).select("+publicKey");
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
user = await new User({
|
|
||||||
email: email,
|
|
||||||
authMethods: [AuthMethod.GITLAB],
|
|
||||||
firstName: profile.displayName,
|
|
||||||
lastName: ""
|
|
||||||
}).save();
|
|
||||||
}
|
|
||||||
|
|
||||||
let isLinkingRequired = false;
|
|
||||||
if (!user.authMethods.includes(AuthMethod.GITLAB)) {
|
|
||||||
isLinkingRequired = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isUserCompleted = !!user.publicKey;
|
|
||||||
const providerAuthToken = createToken({
|
|
||||||
payload: {
|
|
||||||
authTokenType: AuthTokenType.PROVIDER_TOKEN,
|
|
||||||
userId: user._id.toString(),
|
|
||||||
email: user.email,
|
|
||||||
firstName: user.firstName,
|
|
||||||
lastName: user.lastName,
|
|
||||||
authMethod: AuthMethod.GITLAB,
|
|
||||||
isUserCompleted,
|
|
||||||
isLinkingRequired,
|
|
||||||
...(req.query.state ? {
|
|
||||||
callbackPort: req.query.state as string
|
|
||||||
} : {})
|
|
||||||
},
|
|
||||||
expiresIn: await getJwtProviderAuthLifetime(),
|
|
||||||
secret: await getAuthSecret(),
|
|
||||||
});
|
|
||||||
|
|
||||||
req.isUserCompleted = isUserCompleted;
|
|
||||||
req.providerAuthToken = providerAuthToken;
|
|
||||||
return done(null, profile);
|
|
||||||
}
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
passport.use("saml", new MultiSamlStrategy(
|
|
||||||
{
|
|
||||||
passReqToCallback: true,
|
|
||||||
getSamlOptions: async (req: any, done: any) => {
|
|
||||||
const { ssoIdentifier } = req.params;
|
|
||||||
|
|
||||||
const ssoConfig = await getSSOConfigHelper({
|
|
||||||
ssoConfigId: new Types.ObjectId(ssoIdentifier)
|
|
||||||
});
|
|
||||||
|
|
||||||
interface ISAMLConfig {
|
|
||||||
callbackUrl: string;
|
|
||||||
entryPoint: string;
|
|
||||||
issuer: string;
|
|
||||||
cert: string;
|
|
||||||
audience: string;
|
|
||||||
wantAuthnResponseSigned?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const samlConfig: ISAMLConfig = ({
|
|
||||||
callbackUrl: `${await getSiteURL()}/api/v1/sso/saml2/${ssoIdentifier}`,
|
|
||||||
entryPoint: ssoConfig.entryPoint,
|
|
||||||
issuer: ssoConfig.issuer,
|
|
||||||
cert: ssoConfig.cert,
|
|
||||||
audience: await getSiteURL()
|
|
||||||
});
|
|
||||||
|
|
||||||
if (ssoConfig.authProvider.toString() === AuthMethod.JUMPCLOUD_SAML.toString()) {
|
|
||||||
samlConfig.wantAuthnResponseSigned = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ssoConfig.authProvider.toString() === AuthMethod.AZURE_SAML.toString()) {
|
|
||||||
if (req.body.RelayState && JSON.parse(req.body.RelayState).spInitiated) {
|
|
||||||
samlConfig.audience = `spn:${ssoConfig.issuer}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
req.ssoConfig = ssoConfig;
|
|
||||||
|
|
||||||
done(null, samlConfig);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
async (req: any, profile: any, done: any) => {
|
|
||||||
if (!req.ssoConfig.isActive) return done(InternalServerError());
|
|
||||||
|
|
||||||
const organization = await Organization.findById(req.ssoConfig.organization);
|
|
||||||
|
|
||||||
if (!organization) return done(OrganizationNotFoundError());
|
|
||||||
|
|
||||||
const email = profile.email;
|
|
||||||
const firstName = profile.firstName;
|
|
||||||
const lastName = profile.lastName;
|
|
||||||
|
|
||||||
let user = await User.findOne({
|
|
||||||
email
|
|
||||||
}).select("+publicKey");
|
|
||||||
|
|
||||||
if (user) {
|
|
||||||
// if user does not have SAML enabled then update
|
|
||||||
const hasSamlEnabled = user.authMethods
|
|
||||||
.some(
|
|
||||||
(authMethod: AuthMethod) => [
|
|
||||||
AuthMethod.OKTA_SAML,
|
|
||||||
AuthMethod.AZURE_SAML,
|
|
||||||
AuthMethod.JUMPCLOUD_SAML
|
|
||||||
].includes(authMethod)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!hasSamlEnabled) {
|
|
||||||
await User.findByIdAndUpdate(
|
|
||||||
user._id,
|
|
||||||
{
|
|
||||||
authMethods: [req.ssoConfig.authProvider]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
new: true
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let membershipOrg = await MembershipOrg.findOne(
|
|
||||||
{
|
|
||||||
user: user._id,
|
|
||||||
organization: organization._id
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!membershipOrg) {
|
|
||||||
membershipOrg = await new MembershipOrg({
|
|
||||||
inviteEmail: email,
|
|
||||||
user: user._id,
|
|
||||||
organization: organization._id,
|
|
||||||
role: MEMBER,
|
|
||||||
status: ACCEPTED
|
|
||||||
}).save();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (membershipOrg.status === INVITED) {
|
|
||||||
membershipOrg.status = ACCEPTED;
|
|
||||||
await membershipOrg.save();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
user = await new User({
|
|
||||||
email,
|
|
||||||
authMethods: [req.ssoConfig.authProvider],
|
|
||||||
firstName,
|
|
||||||
lastName
|
|
||||||
}).save();
|
|
||||||
|
|
||||||
await new MembershipOrg({
|
|
||||||
inviteEmail: email,
|
|
||||||
user: user._id,
|
|
||||||
organization: organization._id,
|
|
||||||
role: MEMBER,
|
|
||||||
status: INVITED
|
|
||||||
}).save();
|
|
||||||
}
|
|
||||||
|
|
||||||
const isUserCompleted = !!user.publicKey;
|
|
||||||
const providerAuthToken = createToken({
|
|
||||||
payload: {
|
|
||||||
authTokenType: AuthTokenType.PROVIDER_TOKEN,
|
|
||||||
userId: user._id.toString(),
|
|
||||||
email: user.email,
|
|
||||||
firstName,
|
|
||||||
lastName,
|
|
||||||
organizationName: organization?.name,
|
|
||||||
authMethod: req.ssoConfig.authProvider,
|
|
||||||
isUserCompleted,
|
|
||||||
...(req.body.RelayState ? {
|
|
||||||
callbackPort: JSON.parse(req.body.RelayState).callbackPort as string
|
|
||||||
} : {})
|
|
||||||
},
|
|
||||||
expiresIn: await getJwtProviderAuthLifetime(),
|
|
||||||
secret: await getAuthSecret(),
|
|
||||||
});
|
|
||||||
|
|
||||||
req.isUserCompleted = isUserCompleted;
|
|
||||||
req.providerAuthToken = providerAuthToken;
|
|
||||||
|
|
||||||
done(null, profile);
|
|
||||||
}
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
getAuthDataPayloadIdObj,
|
|
||||||
getAuthDataPayloadUserObj,
|
|
||||||
initializePassport,
|
|
||||||
}
|
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export * from "./apiKey";
|
||||||
|
export * from "./apiKeyV2";
|
||||||
|
export * from "./jwt";
|
||||||
|
export * from "./serviceTokenV2";
|
||||||
|
export * from "./serviceTokenV3";
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { AuthData } from "../../../interfaces/middleware";
|
||||||
|
import {
|
||||||
|
ServiceAccount,
|
||||||
|
ServiceTokenData,
|
||||||
|
ServiceTokenDataV3,
|
||||||
|
User
|
||||||
|
} from "../../../models";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an object containing the id of the authentication data payload
|
||||||
|
* @param {AuthData} authData - authentication data object
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const getAuthDataPayloadIdObj = (authData: AuthData) => {
|
||||||
|
if (authData.authPayload instanceof User) {
|
||||||
|
return { userId: authData.authPayload._id };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authData.authPayload instanceof ServiceAccount) {
|
||||||
|
return { serviceAccountId: authData.authPayload._id };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authData.authPayload instanceof ServiceTokenData) {
|
||||||
|
return { serviceTokenDataId: authData.authPayload._id };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authData.authPayload instanceof ServiceTokenDataV3) {
|
||||||
|
return { serviceTokenDataId: authData.authPayload._id };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an object containing the user associated with the authentication data payload
|
||||||
|
* @param {AuthData} authData - authentication data object
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const getAuthDataPayloadUserObj = (authData: AuthData) => {
|
||||||
|
if (authData.authPayload instanceof User) {
|
||||||
|
return { user: authData.authPayload._id };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authData.authPayload instanceof ServiceAccount) {
|
||||||
|
return { user: authData.authPayload.user };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authData.authPayload instanceof ServiceTokenData) {
|
||||||
|
return { user: authData.authPayload.user };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authData.authPayload instanceof ServiceTokenDataV3) {
|
||||||
|
return { user: authData.authPayload.user };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,195 @@
|
|||||||
|
import { AuthData } from "../../../interfaces/middleware";
|
||||||
|
import jwt from "jsonwebtoken";
|
||||||
|
import { getAuthSecret } from "../../../config";
|
||||||
|
import { ActorType } from "../../../ee/models";
|
||||||
|
import { AuthMode, AuthTokenType } from "../../../variables";
|
||||||
|
import { UnauthorizedRequestError } from "../../errors";
|
||||||
|
import {
|
||||||
|
validateAPIKey,
|
||||||
|
validateAPIKeyV2,
|
||||||
|
validateJWT,
|
||||||
|
validateServiceTokenV2,
|
||||||
|
validateServiceTokenV3
|
||||||
|
} from "../authModeValidators";
|
||||||
|
import { getUserAgentType } from "../../posthog";
|
||||||
|
|
||||||
|
export * from "./authDataExtractors";
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import express from "express";
|
||||||
|
import passport from "passport";
|
||||||
|
import {
|
||||||
|
getClientIdGitHubLogin,
|
||||||
|
getClientSecretGitHubLogin,
|
||||||
|
} from "../../../config";
|
||||||
|
import { standardRequest } from "../../../config/request";
|
||||||
|
import { AuthMethod } from "../../../models";
|
||||||
|
import { INTEGRATION_GITHUB_API_URL } from "../../../variables";
|
||||||
|
import { handleSSOUserTokenFlow } from "./helpers";
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
const GitHubStrategy = require("passport-github").Strategy;
|
||||||
|
|
||||||
|
export const initializeGitHubStrategy = async () => {
|
||||||
|
const clientIdGitHubLogin = await getClientIdGitHubLogin();
|
||||||
|
const clientSecretGitHubLogin = await getClientSecretGitHubLogin();
|
||||||
|
if (clientIdGitHubLogin && clientSecretGitHubLogin) {
|
||||||
|
passport.use(
|
||||||
|
new GitHubStrategy({
|
||||||
|
passReqToCallback: true,
|
||||||
|
clientID: clientIdGitHubLogin,
|
||||||
|
clientSecret: clientSecretGitHubLogin,
|
||||||
|
callbackURL: "/api/v1/sso/github",
|
||||||
|
scope: ["user:email"]
|
||||||
|
}, async (req : express.Request, accessToken : any, refreshToken : any, profile : any, done : any) => {
|
||||||
|
interface GitHubEmail {
|
||||||
|
email: string;
|
||||||
|
primary: boolean;
|
||||||
|
verified: boolean;
|
||||||
|
visibility: null | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data }: { data: GitHubEmail[] } = await standardRequest.get(
|
||||||
|
`${INTEGRATION_GITHUB_API_URL}/user/emails`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const primaryEmail = data.filter((gitHubEmail: GitHubEmail) => gitHubEmail.primary)[0];
|
||||||
|
const email = primaryEmail.email;
|
||||||
|
|
||||||
|
const { isUserCompleted, providerAuthToken } = await handleSSOUserTokenFlow({
|
||||||
|
email,
|
||||||
|
firstName: profile.displayName,
|
||||||
|
lastName: "",
|
||||||
|
authMethod: AuthMethod.GITHUB,
|
||||||
|
callbackPort: req.query.state as string
|
||||||
|
});
|
||||||
|
|
||||||
|
req.isUserCompleted = isUserCompleted;
|
||||||
|
req.providerAuthToken = providerAuthToken;
|
||||||
|
return done(null, profile);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import express from "express";
|
||||||
|
import passport from "passport";
|
||||||
|
import {
|
||||||
|
getClientIdGitLabLogin,
|
||||||
|
getClientSecretGitLabLogin,
|
||||||
|
getUrlGitLabLogin
|
||||||
|
} from "../../../config";
|
||||||
|
import { AuthMethod } from "../../../models";
|
||||||
|
import { handleSSOUserTokenFlow } from "./helpers";
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
const GitLabStrategy = require("passport-gitlab2").Strategy;
|
||||||
|
|
||||||
|
export const initializeGitLabStrategy = async () => {
|
||||||
|
const urlGitLab = await getUrlGitLabLogin();
|
||||||
|
const clientIdGitLabLogin = await getClientIdGitLabLogin();
|
||||||
|
const clientSecretGitLabLogin = await getClientSecretGitLabLogin();
|
||||||
|
|
||||||
|
if (urlGitLab && clientIdGitLabLogin && clientSecretGitLabLogin) {
|
||||||
|
passport.use(
|
||||||
|
new GitLabStrategy({
|
||||||
|
passReqToCallback: true,
|
||||||
|
clientID: clientIdGitLabLogin,
|
||||||
|
clientSecret: clientSecretGitLabLogin,
|
||||||
|
callbackURL: "/api/v1/sso/gitlab",
|
||||||
|
baseURL: urlGitLab
|
||||||
|
}, async (req : express.Request, accessToken : any, refreshToken : any, profile : any, done : any) => {
|
||||||
|
const email = profile.emails[0].value;
|
||||||
|
|
||||||
|
const { isUserCompleted, providerAuthToken } = await handleSSOUserTokenFlow({
|
||||||
|
email,
|
||||||
|
firstName: profile.displayName,
|
||||||
|
lastName: "",
|
||||||
|
authMethod: AuthMethod.GITLAB,
|
||||||
|
callbackPort: req.query.state as string
|
||||||
|
});
|
||||||
|
|
||||||
|
req.isUserCompleted = isUserCompleted;
|
||||||
|
req.providerAuthToken = providerAuthToken;
|
||||||
|
return done(null, profile);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import express from "express";
|
||||||
|
import passport from "passport";
|
||||||
|
import { getClientIdGoogleLogin, getClientSecretGoogleLogin } from "../../../config";
|
||||||
|
import { AuthMethod } from "../../../models";
|
||||||
|
|
||||||
|
import { handleSSOUserTokenFlow } from "./helpers";
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
const GoogleStrategy = require("passport-google-oauth20").Strategy;
|
||||||
|
|
||||||
|
export const initializeGoogleStrategy = async () => {
|
||||||
|
const clientIdGoogleLogin = await getClientIdGoogleLogin();
|
||||||
|
const clientSecretGoogleLogin = await getClientSecretGoogleLogin();
|
||||||
|
|
||||||
|
if (clientIdGoogleLogin && clientSecretGoogleLogin) {
|
||||||
|
passport.use(new GoogleStrategy({
|
||||||
|
passReqToCallback: true,
|
||||||
|
clientID: clientIdGoogleLogin,
|
||||||
|
clientSecret: clientSecretGoogleLogin,
|
||||||
|
callbackURL: "/api/v1/sso/google",
|
||||||
|
scope: ["profile", " email"],
|
||||||
|
}, async (
|
||||||
|
req: express.Request,
|
||||||
|
accessToken: string,
|
||||||
|
refreshToken: string,
|
||||||
|
profile: any,
|
||||||
|
done: any
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const email = profile.emails[0].value;
|
||||||
|
|
||||||
|
const { isUserCompleted, providerAuthToken } = await handleSSOUserTokenFlow({
|
||||||
|
email,
|
||||||
|
firstName: profile.name.givenName,
|
||||||
|
lastName: profile.name.familyName,
|
||||||
|
authMethod: AuthMethod.GOOGLE,
|
||||||
|
callbackPort: req.query.state as string
|
||||||
|
});
|
||||||
|
|
||||||
|
req.isUserCompleted = isUserCompleted;
|
||||||
|
req.providerAuthToken = providerAuthToken;
|
||||||
|
done(null, profile);
|
||||||
|
} catch (err) {
|
||||||
|
done(null, false);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import {
|
||||||
|
AuthMethod,
|
||||||
|
User
|
||||||
|
} from "../../../models";
|
||||||
|
import { createToken } from "../../../helpers/auth";
|
||||||
|
import { AuthTokenType } from "../../../variables";
|
||||||
|
import { getAuthSecret, getJwtProviderAuthLifetime} from "../../../config";
|
||||||
|
|
||||||
|
interface SSOUserTokenFlowParams {
|
||||||
|
email: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
authMethod: AuthMethod;
|
||||||
|
callbackPort?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const handleSSOUserTokenFlow = async ({
|
||||||
|
email,
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
authMethod,
|
||||||
|
callbackPort
|
||||||
|
}: SSOUserTokenFlowParams) => {
|
||||||
|
let user = await User.findOne({
|
||||||
|
email
|
||||||
|
}).select("+publicKey");
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
user = await new User({
|
||||||
|
email,
|
||||||
|
authMethods: [authMethod],
|
||||||
|
firstName,
|
||||||
|
lastName
|
||||||
|
}).save();
|
||||||
|
}
|
||||||
|
|
||||||
|
let isLinkingRequired = false;
|
||||||
|
if (!user.authMethods.includes(authMethod)) {
|
||||||
|
isLinkingRequired = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isUserCompleted = !!user.publicKey;
|
||||||
|
const providerAuthToken = createToken({
|
||||||
|
payload: {
|
||||||
|
authTokenType: AuthTokenType.PROVIDER_TOKEN,
|
||||||
|
userId: user._id.toString(),
|
||||||
|
email: user.email,
|
||||||
|
firstName: user.firstName,
|
||||||
|
lastName: user.lastName,
|
||||||
|
authMethod,
|
||||||
|
isUserCompleted,
|
||||||
|
isLinkingRequired,
|
||||||
|
...(callbackPort ? {
|
||||||
|
callbackPort
|
||||||
|
} : {})
|
||||||
|
},
|
||||||
|
expiresIn: await getJwtProviderAuthLifetime(),
|
||||||
|
secret: await getAuthSecret(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return { isUserCompleted, providerAuthToken };
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export { initializeGoogleStrategy } from "./google";
|
||||||
|
export { initializeGitHubStrategy } from "./github";
|
||||||
|
export { initializeGitLabStrategy } from "./gitlab";
|
||||||
|
export { initializeSamlStrategy } from "./saml";
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
import passport from "passport";
|
||||||
|
import {
|
||||||
|
getAuthSecret,
|
||||||
|
getJwtProviderAuthLifetime,
|
||||||
|
getSiteURL
|
||||||
|
} from "../../../config";
|
||||||
|
import {
|
||||||
|
AuthMethod,
|
||||||
|
MembershipOrg,
|
||||||
|
Organization,
|
||||||
|
User
|
||||||
|
} from "../../../models";
|
||||||
|
import {
|
||||||
|
createToken
|
||||||
|
} from "../../../helpers/auth";
|
||||||
|
import {
|
||||||
|
ACCEPTED,
|
||||||
|
AuthTokenType,
|
||||||
|
INVITED,
|
||||||
|
MEMBER
|
||||||
|
} from "../../../variables";
|
||||||
|
import { Types } from "mongoose";
|
||||||
|
import { getSSOConfigHelper } from "../../../ee/helpers/organizations";
|
||||||
|
import { InternalServerError, OrganizationNotFoundError } from "../../errors";
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
const { MultiSamlStrategy } = require("@node-saml/passport-saml");
|
||||||
|
|
||||||
|
export const initializeSamlStrategy = async () => {
|
||||||
|
passport.use("saml", new MultiSamlStrategy(
|
||||||
|
{
|
||||||
|
passReqToCallback: true,
|
||||||
|
getSamlOptions: async (req: any, done: any) => {
|
||||||
|
const { ssoIdentifier } = req.params;
|
||||||
|
|
||||||
|
const ssoConfig = await getSSOConfigHelper({
|
||||||
|
ssoConfigId: new Types.ObjectId(ssoIdentifier)
|
||||||
|
});
|
||||||
|
|
||||||
|
interface ISAMLConfig {
|
||||||
|
callbackUrl: string;
|
||||||
|
entryPoint: string;
|
||||||
|
issuer: string;
|
||||||
|
cert: string;
|
||||||
|
audience: string;
|
||||||
|
wantAuthnResponseSigned?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const samlConfig: ISAMLConfig = ({
|
||||||
|
callbackUrl: `${await getSiteURL()}/api/v1/sso/saml2/${ssoIdentifier}`,
|
||||||
|
entryPoint: ssoConfig.entryPoint,
|
||||||
|
issuer: ssoConfig.issuer,
|
||||||
|
cert: ssoConfig.cert,
|
||||||
|
audience: await getSiteURL()
|
||||||
|
});
|
||||||
|
|
||||||
|
if (ssoConfig.authProvider.toString() === AuthMethod.JUMPCLOUD_SAML.toString()) {
|
||||||
|
samlConfig.wantAuthnResponseSigned = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ssoConfig.authProvider.toString() === AuthMethod.AZURE_SAML.toString()) {
|
||||||
|
if (req.body.RelayState && JSON.parse(req.body.RelayState).spInitiated) {
|
||||||
|
samlConfig.audience = `spn:${ssoConfig.issuer}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
req.ssoConfig = ssoConfig;
|
||||||
|
|
||||||
|
done(null, samlConfig);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
async (req: any, profile: any, done: any) => {
|
||||||
|
if (!req.ssoConfig.isActive) return done(InternalServerError());
|
||||||
|
|
||||||
|
const organization = await Organization.findById(req.ssoConfig.organization);
|
||||||
|
|
||||||
|
if (!organization) return done(OrganizationNotFoundError());
|
||||||
|
|
||||||
|
const email = profile.email;
|
||||||
|
const firstName = profile.firstName;
|
||||||
|
const lastName = profile.lastName;
|
||||||
|
|
||||||
|
let user = await User.findOne({
|
||||||
|
email
|
||||||
|
}).select("+publicKey");
|
||||||
|
|
||||||
|
if (user) {
|
||||||
|
// if user does not have SAML enabled then update
|
||||||
|
const hasSamlEnabled = user.authMethods
|
||||||
|
.some(
|
||||||
|
(authMethod: AuthMethod) => [
|
||||||
|
AuthMethod.OKTA_SAML,
|
||||||
|
AuthMethod.AZURE_SAML,
|
||||||
|
AuthMethod.JUMPCLOUD_SAML
|
||||||
|
].includes(authMethod)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!hasSamlEnabled) {
|
||||||
|
await User.findByIdAndUpdate(
|
||||||
|
user._id,
|
||||||
|
{
|
||||||
|
authMethods: [req.ssoConfig.authProvider]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
new: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let membershipOrg = await MembershipOrg.findOne(
|
||||||
|
{
|
||||||
|
user: user._id,
|
||||||
|
organization: organization._id
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!membershipOrg) {
|
||||||
|
membershipOrg = await new MembershipOrg({
|
||||||
|
inviteEmail: email,
|
||||||
|
user: user._id,
|
||||||
|
organization: organization._id,
|
||||||
|
role: MEMBER,
|
||||||
|
status: ACCEPTED
|
||||||
|
}).save();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (membershipOrg.status === INVITED) {
|
||||||
|
membershipOrg.status = ACCEPTED;
|
||||||
|
await membershipOrg.save();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
user = await new User({
|
||||||
|
email,
|
||||||
|
authMethods: [req.ssoConfig.authProvider],
|
||||||
|
firstName,
|
||||||
|
lastName
|
||||||
|
}).save();
|
||||||
|
|
||||||
|
await new MembershipOrg({
|
||||||
|
inviteEmail: email,
|
||||||
|
user: user._id,
|
||||||
|
organization: organization._id,
|
||||||
|
role: MEMBER,
|
||||||
|
status: INVITED
|
||||||
|
}).save();
|
||||||
|
}
|
||||||
|
|
||||||
|
const isUserCompleted = !!user.publicKey;
|
||||||
|
const providerAuthToken = createToken({
|
||||||
|
payload: {
|
||||||
|
authTokenType: AuthTokenType.PROVIDER_TOKEN,
|
||||||
|
userId: user._id.toString(),
|
||||||
|
email: user.email,
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
organizationName: organization?.name,
|
||||||
|
authMethod: req.ssoConfig.authProvider,
|
||||||
|
isUserCompleted,
|
||||||
|
...(req.body.RelayState ? {
|
||||||
|
callbackPort: JSON.parse(req.body.RelayState).callbackPort as string
|
||||||
|
} : {})
|
||||||
|
},
|
||||||
|
expiresIn: await getJwtProviderAuthLifetime(),
|
||||||
|
secret: await getAuthSecret(),
|
||||||
|
});
|
||||||
|
|
||||||
|
req.isUserCompleted = isUserCompleted;
|
||||||
|
req.providerAuthToken = providerAuthToken;
|
||||||
|
|
||||||
|
done(null, profile);
|
||||||
|
}
|
||||||
|
));
|
||||||
|
}
|
||||||
@@ -26,7 +26,12 @@ import {
|
|||||||
reencryptSecretBlindIndexDataSalts
|
reencryptSecretBlindIndexDataSalts
|
||||||
} from "./reencryptData";
|
} from "./reencryptData";
|
||||||
import { getMongoURL, getNodeEnv, getRedisUrl, getSentryDSN } from "../../config";
|
import { getMongoURL, getNodeEnv, getRedisUrl, getSentryDSN } from "../../config";
|
||||||
import { initializePassport } from "../auth";
|
import {
|
||||||
|
initializeGitHubStrategy,
|
||||||
|
initializeGitLabStrategy,
|
||||||
|
initializeGoogleStrategy,
|
||||||
|
initializeSamlStrategy
|
||||||
|
} from "../authn/passport";
|
||||||
import { logger } from "../logging";
|
import { logger } from "../logging";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -55,7 +60,11 @@ export const setup = async () => {
|
|||||||
// initializing global feature set
|
// initializing global feature set
|
||||||
await EELicenseService.initGlobalFeatureSet();
|
await EELicenseService.initGlobalFeatureSet();
|
||||||
|
|
||||||
await initializePassport();
|
// initializing auth strategies
|
||||||
|
await initializeGoogleStrategy();
|
||||||
|
await initializeGitHubStrategy()
|
||||||
|
await initializeGitLabStrategy();
|
||||||
|
await initializeSamlStrategy();
|
||||||
|
|
||||||
// re-encrypt any data previously encrypted under server hex 128-bit ENCRYPTION_KEY
|
// re-encrypt any data previously encrypted under server hex 128-bit ENCRYPTION_KEY
|
||||||
// to base64 256-bit ROOT_ENCRYPTION_KEY
|
// to base64 256-bit ROOT_ENCRYPTION_KEY
|
||||||
|
|||||||
@@ -60,6 +60,12 @@ import { checkIPAgainstBlocklist } from "../utils/ip";
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const RefreshTokenV3 = z.object({
|
||||||
|
body: z.object({
|
||||||
|
refresh_token: z.string().trim()
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
export const CreateServiceTokenV3 = z.object({
|
export const CreateServiceTokenV3 = z.object({
|
||||||
body: z.object({
|
body: z.object({
|
||||||
name: z.string().trim(),
|
name: z.string().trim(),
|
||||||
@@ -80,8 +86,10 @@ export const CreateServiceTokenV3 = z.object({
|
|||||||
.array()
|
.array()
|
||||||
.min(1),
|
.min(1),
|
||||||
expiresIn: z.number().optional(),
|
expiresIn: z.number().optional(),
|
||||||
|
accessTokenTTL: z.number().int().min(1),
|
||||||
encryptedKey: z.string().trim(),
|
encryptedKey: z.string().trim(),
|
||||||
nonce: z.string().trim()
|
nonce: z.string().trim(),
|
||||||
|
isRefreshTokenRotationEnabled: z.boolean().default(false)
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -108,7 +116,9 @@ export const UpdateServiceTokenV3 = z.object({
|
|||||||
.array()
|
.array()
|
||||||
.min(1)
|
.min(1)
|
||||||
.optional(),
|
.optional(),
|
||||||
expiresIn: z.number().optional()
|
expiresIn: z.number().optional(),
|
||||||
|
accessTokenTTL: z.number().int().min(1).optional(),
|
||||||
|
isRefreshTokenRotationEnabled: z.boolean().optional()
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,22 @@
|
|||||||
|
// TODO: merge [AuthTokenType] and [AuthMode]
|
||||||
|
|
||||||
export enum AuthTokenType {
|
export enum AuthTokenType {
|
||||||
ACCESS_TOKEN = "accessToken",
|
ACCESS_TOKEN = "accessToken",
|
||||||
REFRESH_TOKEN = "refreshToken",
|
REFRESH_TOKEN = "refreshToken",
|
||||||
SIGNUP_TOKEN = "signupToken",
|
SIGNUP_TOKEN = "signupToken", // TODO: remove in favor of claim
|
||||||
MFA_TOKEN = "mfaToken",
|
MFA_TOKEN = "mfaToken", // TODO: remove in favor of claim
|
||||||
PROVIDER_TOKEN = "providerToken",
|
PROVIDER_TOKEN = "providerToken", // TODO: remove in favor of claim
|
||||||
API_KEY = "apiKey"
|
API_KEY = "apiKey",
|
||||||
|
SERVICE_ACCESS_TOKEN = "serviceAccessToken",
|
||||||
|
SERVICE_REFRESH_TOKEN = "serviceRefreshToken"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum AuthMode {
|
export enum AuthMode {
|
||||||
JWT = "jwt",
|
JWT = "jwt",
|
||||||
SERVICE_TOKEN = "serviceToken",
|
SERVICE_TOKEN = "serviceToken",
|
||||||
SERVICE_TOKEN_V3 = "serviceTokenV3",
|
SERVICE_ACCESS_TOKEN = "serviceAccessToken",
|
||||||
API_KEY = "apiKey"
|
API_KEY = "apiKey",
|
||||||
|
API_KEY_V2 = "apiKeyV2"
|
||||||
}
|
}
|
||||||
|
|
||||||
export const K8_USER_AGENT_NAME = "k8-operator"
|
export const K8_USER_AGENT_NAME = "k8-operator"
|
||||||
@@ -88,14 +88,18 @@ export const useUpdateServiceTokenV3 = () => {
|
|||||||
isActive,
|
isActive,
|
||||||
scopes,
|
scopes,
|
||||||
trustedIps,
|
trustedIps,
|
||||||
expiresIn
|
expiresIn,
|
||||||
|
accessTokenTTL,
|
||||||
|
isRefreshTokenRotationEnabled
|
||||||
}) => {
|
}) => {
|
||||||
const { data: { serviceTokenData } } = await apiRequest.patch(`/api/v3/service-token/${serviceTokenDataId}`, {
|
const { data: { serviceTokenData } } = await apiRequest.patch(`/api/v3/service-token/${serviceTokenDataId}`, {
|
||||||
name,
|
name,
|
||||||
isActive,
|
isActive,
|
||||||
scopes,
|
scopes,
|
||||||
trustedIps,
|
trustedIps,
|
||||||
expiresIn
|
expiresIn,
|
||||||
|
accessTokenTTL,
|
||||||
|
isRefreshTokenRotationEnabled
|
||||||
});
|
});
|
||||||
|
|
||||||
return serviceTokenData;
|
return serviceTokenData;
|
||||||
|
|||||||
@@ -56,11 +56,15 @@ export type ServiceTokenDataV3 = {
|
|||||||
name: string;
|
name: string;
|
||||||
workspace: string;
|
workspace: string;
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
lastUsed?: string;
|
refreshTokenLastUsed?: string;
|
||||||
usageCount: number;
|
accessTokenLastUsed?: string;
|
||||||
|
refreshTokenUsageCount: number;
|
||||||
|
accessTokenUsageCount: number;
|
||||||
scopes: ServiceTokenV3Scope[];
|
scopes: ServiceTokenV3Scope[];
|
||||||
trustedIps: ServiceTokenV3TrustedIp[];
|
trustedIps: ServiceTokenV3TrustedIp[];
|
||||||
expiresAt?: string;
|
expiresAt?: string;
|
||||||
|
accessTokenTTL: number;
|
||||||
|
isRefreshTokenRotationEnabled: boolean;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
};
|
};
|
||||||
@@ -74,12 +78,14 @@ export type CreateServiceTokenDataV3DTO = {
|
|||||||
ipAddress: string;
|
ipAddress: string;
|
||||||
}[];
|
}[];
|
||||||
expiresIn?: number;
|
expiresIn?: number;
|
||||||
|
accessTokenTTL: number;
|
||||||
encryptedKey: string;
|
encryptedKey: string;
|
||||||
nonce: string;
|
nonce: string;
|
||||||
|
isRefreshTokenRotationEnabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CreateServiceTokenDataV3Res = {
|
export type CreateServiceTokenDataV3Res = {
|
||||||
serviceToken: string;
|
refreshToken: string;
|
||||||
serviceTokenData: ServiceTokenDataV3;
|
serviceTokenData: ServiceTokenDataV3;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,6 +98,8 @@ export type UpdateServiceTokenDataV3DTO = {
|
|||||||
ipAddress: string;
|
ipAddress: string;
|
||||||
}[];
|
}[];
|
||||||
expiresIn?: number;
|
expiresIn?: number;
|
||||||
|
accessTokenTTL?: number;
|
||||||
|
isRefreshTokenRotationEnabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DeleteServiceTokenDataV3DTO = {
|
export type DeleteServiceTokenDataV3DTO = {
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
// import { APIKeyV2Section } from "../APIKeyV2Section";
|
|
||||||
import { APIKeySection } from "../APIKeySection";
|
import { APIKeySection } from "../APIKeySection";
|
||||||
|
// import { APIKeyV2Section } from "../APIKeyV2Section";
|
||||||
|
|
||||||
export const PersonalAPIKeyTab = () => {
|
export const PersonalAPIKeyTab = () => {
|
||||||
return (
|
return (
|
||||||
|
|||||||
+366
-234
@@ -1,8 +1,9 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
||||||
import { faPlus,faXmark } from "@fortawesome/free-solid-svg-icons";
|
import { faCheck, faCopy,faPlus, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { yupResolver } from "@hookform/resolvers/yup";
|
import { yupResolver } from "@hookform/resolvers/yup";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
import nacl from "tweetnacl";
|
import nacl from "tweetnacl";
|
||||||
import { encodeBase64 } from "tweetnacl-util";
|
import { encodeBase64 } from "tweetnacl-util";
|
||||||
import * as yup from "yup";
|
import * as yup from "yup";
|
||||||
@@ -21,12 +22,17 @@ import {
|
|||||||
ModalContent,
|
ModalContent,
|
||||||
Select,
|
Select,
|
||||||
SelectItem,
|
SelectItem,
|
||||||
UpgradePlanModal
|
Switch,
|
||||||
} from "@app/components/v2";
|
Tab,
|
||||||
|
TabList,
|
||||||
|
TabPanel,
|
||||||
|
Tabs,
|
||||||
|
UpgradePlanModal} from "@app/components/v2";
|
||||||
import {
|
import {
|
||||||
useSubscription,
|
useSubscription,
|
||||||
useWorkspace
|
useWorkspace
|
||||||
} from "@app/context";
|
} from "@app/context";
|
||||||
|
import { useToggle } from "@app/hooks";
|
||||||
import {
|
import {
|
||||||
useCreateServiceTokenV3,
|
useCreateServiceTokenV3,
|
||||||
useGetUserWsKey,
|
useGetUserWsKey,
|
||||||
@@ -41,8 +47,13 @@ import {
|
|||||||
} from "@app/hooks/api/serviceTokens/types";
|
} from "@app/hooks/api/serviceTokens/types";
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
|
enum TabSections {
|
||||||
|
General = "general",
|
||||||
|
Advanced = "advanced"
|
||||||
|
}
|
||||||
|
|
||||||
const expirations = [
|
const expirations = [
|
||||||
{ label: "Never", value: undefined },
|
{ label: "Never", value: "" },
|
||||||
{ label: "1 day", value: "86400" },
|
{ label: "1 day", value: "86400" },
|
||||||
{ label: "7 days", value: "604800" },
|
{ label: "7 days", value: "604800" },
|
||||||
{ label: "1 month", value: "2592000" },
|
{ label: "1 month", value: "2592000" },
|
||||||
@@ -60,6 +71,17 @@ const permissionsMap: {
|
|||||||
const schema = yup.object({
|
const schema = yup.object({
|
||||||
name: yup.string().required("ST V3 name is required"),
|
name: yup.string().required("ST V3 name is required"),
|
||||||
expiresIn: yup.string(),
|
expiresIn: yup.string(),
|
||||||
|
accessTokenTTL: yup
|
||||||
|
.string()
|
||||||
|
.test("is-positive-integer", "Access Token TTL must be a positive integer", (value) => {
|
||||||
|
if (typeof value === "undefined") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const num = parseInt(value, 10);
|
||||||
|
return !Number.isNaN(num) && num > 0 && String(num) === value;
|
||||||
|
})
|
||||||
|
.required("Access Token TTL is required"),
|
||||||
scopes: yup
|
scopes: yup
|
||||||
.array(
|
.array(
|
||||||
yup.object({
|
yup.object({
|
||||||
@@ -79,14 +101,15 @@ const schema = yup.object({
|
|||||||
.required()
|
.required()
|
||||||
.label("Scope"),
|
.label("Scope"),
|
||||||
trustedIps: yup
|
trustedIps: yup
|
||||||
.array(
|
.array(
|
||||||
yup.object({
|
yup.object({
|
||||||
ipAddress: yup.string().max(50).required().label("IP Address")
|
ipAddress: yup.string().max(50).required().label("IP Address")
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.min(1)
|
.min(1)
|
||||||
.required()
|
.required()
|
||||||
.label("Trusted IP")
|
.label("Trusted IP"),
|
||||||
|
isRefreshTokenRotationEnabled: yup.boolean().default(false)
|
||||||
}).required();
|
}).required();
|
||||||
|
|
||||||
export type FormData = yup.InferType<typeof schema>;
|
export type FormData = yup.InferType<typeof schema>;
|
||||||
@@ -102,6 +125,9 @@ export const AddServiceTokenV3Modal = ({
|
|||||||
handlePopUpOpen,
|
handlePopUpOpen,
|
||||||
handlePopUpToggle
|
handlePopUpToggle
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
|
const [newServiceTokenJSON, setNewServiceTokenJSON] = useState("");
|
||||||
|
const [isServiceTokenJSONCopied, setIsServiceTokenJSONCopied] = useToggle(false);
|
||||||
|
|
||||||
const { subscription } = useSubscription();
|
const { subscription } = useSubscription();
|
||||||
const { currentWorkspace } = useWorkspace();
|
const { currentWorkspace } = useWorkspace();
|
||||||
|
|
||||||
@@ -118,6 +144,7 @@ export const AddServiceTokenV3Modal = ({
|
|||||||
resolver: yupResolver(schema),
|
resolver: yupResolver(schema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: "",
|
name: "",
|
||||||
|
accessTokenTTL: "7200",
|
||||||
scopes: [{
|
scopes: [{
|
||||||
permission: "read",
|
permission: "read",
|
||||||
environment: currentWorkspace?.environments?.[0]?.slug,
|
environment: currentWorkspace?.environments?.[0]?.slug,
|
||||||
@@ -128,6 +155,21 @@ export const AddServiceTokenV3Modal = ({
|
|||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let timer: NodeJS.Timeout;
|
||||||
|
|
||||||
|
if (isServiceTokenJSONCopied) {
|
||||||
|
timer = setTimeout(() => setIsServiceTokenJSONCopied.off(), 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}, [setIsServiceTokenJSONCopied]);
|
||||||
|
|
||||||
|
const copyTokenToClipboard = () => {
|
||||||
|
navigator.clipboard.writeText(newServiceTokenJSON);
|
||||||
|
setIsServiceTokenJSONCopied.on();
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const serviceTokenData = popUp?.serviceTokenV3?.data as {
|
const serviceTokenData = popUp?.serviceTokenV3?.data as {
|
||||||
@@ -135,6 +177,8 @@ export const AddServiceTokenV3Modal = ({
|
|||||||
name: string;
|
name: string;
|
||||||
scopes: ServiceTokenV3Scope[];
|
scopes: ServiceTokenV3Scope[];
|
||||||
trustedIps: ServiceTokenV3TrustedIp[];
|
trustedIps: ServiceTokenV3TrustedIp[];
|
||||||
|
accessTokenTTL: number;
|
||||||
|
isRefreshTokenRotationEnabled: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (serviceTokenData) {
|
if (serviceTokenData) {
|
||||||
@@ -163,11 +207,14 @@ export const AddServiceTokenV3Modal = ({
|
|||||||
return ({
|
return ({
|
||||||
ipAddress: `${ipAddress}${prefix !== undefined ? `/${prefix}` : ""}`
|
ipAddress: `${ipAddress}${prefix !== undefined ? `/${prefix}` : ""}`
|
||||||
});
|
});
|
||||||
})
|
}),
|
||||||
|
accessTokenTTL: String(serviceTokenData.accessTokenTTL),
|
||||||
|
isRefreshTokenRotationEnabled: serviceTokenData.isRefreshTokenRotationEnabled
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
reset({
|
reset({
|
||||||
name: "",
|
name: "",
|
||||||
|
accessTokenTTL: "7200",
|
||||||
scopes: [{
|
scopes: [{
|
||||||
permission: "read",
|
permission: "read",
|
||||||
environment: currentWorkspace?.environments?.[0]?.slug,
|
environment: currentWorkspace?.environments?.[0]?.slug,
|
||||||
@@ -186,8 +233,10 @@ export const AddServiceTokenV3Modal = ({
|
|||||||
const onFormSubmit = async ({
|
const onFormSubmit = async ({
|
||||||
name,
|
name,
|
||||||
expiresIn,
|
expiresIn,
|
||||||
|
accessTokenTTL,
|
||||||
scopes,
|
scopes,
|
||||||
trustedIps
|
trustedIps,
|
||||||
|
isRefreshTokenRotationEnabled
|
||||||
}: FormData) => {
|
}: FormData) => {
|
||||||
try {
|
try {
|
||||||
const serviceTokenData = popUp?.serviceTokenV3?.data as {
|
const serviceTokenData = popUp?.serviceTokenV3?.data as {
|
||||||
@@ -213,8 +262,12 @@ export const AddServiceTokenV3Modal = ({
|
|||||||
name,
|
name,
|
||||||
scopes: reformattedScopes,
|
scopes: reformattedScopes,
|
||||||
trustedIps,
|
trustedIps,
|
||||||
expiresIn: expiresIn === "" ? undefined : Number(expiresIn)
|
expiresIn: expiresIn === "" ? undefined : Number(expiresIn),
|
||||||
|
accessTokenTTL: Number(accessTokenTTL),
|
||||||
|
isRefreshTokenRotationEnabled
|
||||||
});
|
});
|
||||||
|
|
||||||
|
handlePopUpToggle("serviceTokenV3", false);
|
||||||
} else {
|
} else {
|
||||||
// create
|
// create
|
||||||
if (!currentWorkspace?._id) return;
|
if (!currentWorkspace?._id) return;
|
||||||
@@ -239,24 +292,29 @@ export const AddServiceTokenV3Modal = ({
|
|||||||
privateKey: localStorage.getItem("PRIVATE_KEY") as string
|
privateKey: localStorage.getItem("PRIVATE_KEY") as string
|
||||||
});
|
});
|
||||||
|
|
||||||
const { serviceToken } = await createMutateAsync({
|
const { refreshToken } = await createMutateAsync({
|
||||||
name,
|
name,
|
||||||
workspaceId: currentWorkspace._id,
|
workspaceId: currentWorkspace._id,
|
||||||
publicKey,
|
publicKey,
|
||||||
scopes: reformattedScopes,
|
scopes: reformattedScopes,
|
||||||
trustedIps,
|
trustedIps,
|
||||||
expiresIn: expiresIn === "" ? undefined : Number(expiresIn),
|
expiresIn: expiresIn === "" ? undefined : Number(expiresIn),
|
||||||
|
accessTokenTTL: Number(accessTokenTTL),
|
||||||
encryptedKey: ciphertext,
|
encryptedKey: ciphertext,
|
||||||
nonce
|
nonce,
|
||||||
|
isRefreshTokenRotationEnabled
|
||||||
});
|
});
|
||||||
|
|
||||||
const downloadData = {
|
const downloadData = {
|
||||||
publicKey,
|
public_key: publicKey,
|
||||||
privateKey,
|
private_key: privateKey,
|
||||||
serviceToken
|
refresh_token: refreshToken
|
||||||
};
|
};
|
||||||
|
|
||||||
const blob = new Blob([JSON.stringify(downloadData, null, 2)], { type: "application/json" });
|
const serviceTokenJSON = JSON.stringify(downloadData, null, 2);
|
||||||
|
setNewServiceTokenJSON(serviceTokenJSON);
|
||||||
|
|
||||||
|
const blob = new Blob([serviceTokenJSON], { type: "application/json" });
|
||||||
const href = URL.createObjectURL(blob);
|
const href = URL.createObjectURL(blob);
|
||||||
const link = document.createElement("a");
|
const link = document.createElement("a");
|
||||||
link.href = href;
|
link.href = href;
|
||||||
@@ -272,7 +330,6 @@ export const AddServiceTokenV3Modal = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
reset();
|
reset();
|
||||||
handlePopUpToggle("serviceTokenV3", false);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
createNotification({
|
createNotification({
|
||||||
@@ -281,6 +338,8 @@ export const AddServiceTokenV3Modal = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hasServiceTokenJSON = Boolean(newServiceTokenJSON);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
@@ -288,232 +347,305 @@ export const AddServiceTokenV3Modal = ({
|
|||||||
onOpenChange={(isOpen) => {
|
onOpenChange={(isOpen) => {
|
||||||
handlePopUpToggle("serviceTokenV3", isOpen);
|
handlePopUpToggle("serviceTokenV3", isOpen);
|
||||||
reset();
|
reset();
|
||||||
|
setNewServiceTokenJSON("");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ModalContent title={`${popUp?.serviceTokenV3?.data ? "Update" : "Create"} Service Token V3`}>
|
<ModalContent title={`${popUp?.serviceTokenV3?.data ? "Update" : "Create"} Service Token V3`}>
|
||||||
<form onSubmit={handleSubmit(onFormSubmit)}>
|
{!hasServiceTokenJSON ? (
|
||||||
<Controller
|
<form onSubmit={handleSubmit(onFormSubmit)}>
|
||||||
control={control}
|
<Tabs defaultValue={TabSections.General}>
|
||||||
defaultValue=""
|
<TabList>
|
||||||
name="name"
|
<div className="flex flex-row border-b border-mineshaft-600 w-full">
|
||||||
render={({ field, fieldState: { error } }) => (
|
<Tab value={TabSections.General}>General</Tab>
|
||||||
<FormControl
|
<Tab value={TabSections.Advanced}>Advanced</Tab>
|
||||||
label="Name"
|
</div>
|
||||||
isError={Boolean(error)}
|
</TabList>
|
||||||
errorText={error?.message}
|
<TabPanel value={TabSections.General}>
|
||||||
>
|
<motion.div
|
||||||
<Input
|
key="panel-1"
|
||||||
{...field}
|
transition={{ duration: 0.15 }}
|
||||||
placeholder="My ST V3"
|
initial={{ opacity: 0, translateX: 30 }}
|
||||||
/>
|
animate={{ opacity: 1, translateX: 0 }}
|
||||||
</FormControl>
|
exit={{ opacity: 0, translateX: 30 }}
|
||||||
)}
|
|
||||||
/>
|
|
||||||
{tokenScopes.map(({ id }, index) => (
|
|
||||||
<div className="flex items-end space-x-2 mb-3" key={id}>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name={`scopes.${index}.permission`}
|
|
||||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
className="mb-0"
|
|
||||||
label={index === 0 ? "Permission" : undefined}
|
|
||||||
errorText={error?.message}
|
|
||||||
isError={Boolean(error)}
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
defaultValue={field.value}
|
|
||||||
{...field}
|
|
||||||
onValueChange={(e) => onChange(e)}
|
|
||||||
className="w-36"
|
|
||||||
>
|
|
||||||
<SelectItem value="read" key="st-v3-read">
|
|
||||||
Read
|
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="readWrite" key="st-v3-write">
|
|
||||||
Read & Write
|
|
||||||
</SelectItem>
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name={`scopes.${index}.environment`}
|
|
||||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
className="mb-0"
|
|
||||||
label={index === 0 ? "Environment" : undefined}
|
|
||||||
errorText={error?.message}
|
|
||||||
isError={Boolean(error)}
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
defaultValue={field.value}
|
|
||||||
{...field}
|
|
||||||
onValueChange={(e) => onChange(e)}
|
|
||||||
className="w-36"
|
|
||||||
>
|
|
||||||
{currentWorkspace?.environments.map(({ name, slug }) => (
|
|
||||||
<SelectItem value={slug} key={slug}>
|
|
||||||
{name}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
control={control}
|
|
||||||
name={`scopes.${index}.secretPath`}
|
|
||||||
defaultValue="/"
|
|
||||||
render={({ field, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
className="mb-0 flex-grow"
|
|
||||||
label={index === 0 ? "Secrets Path" : undefined}
|
|
||||||
isError={Boolean(error)}
|
|
||||||
errorText={error?.message}
|
|
||||||
>
|
|
||||||
<Input {...field} placeholder="can be /, /nested/**, /**/deep" />
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<IconButton
|
|
||||||
onClick={() => remove(index)}
|
|
||||||
size="lg"
|
|
||||||
colorSchema="danger"
|
|
||||||
variant="plain"
|
|
||||||
ariaLabel="update"
|
|
||||||
className="p-3"
|
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faXmark} />
|
<Controller
|
||||||
</IconButton>
|
control={control}
|
||||||
</div>
|
defaultValue=""
|
||||||
))}
|
name="name"
|
||||||
<div className="my-4 ml-1">
|
render={({ field, fieldState: { error } }) => (
|
||||||
<Button
|
<FormControl
|
||||||
variant="outline_bg"
|
label="Name"
|
||||||
onClick={() =>
|
isError={Boolean(error)}
|
||||||
append({
|
errorText={error?.message}
|
||||||
permission: "read",
|
>
|
||||||
environment: currentWorkspace?.environments?.[0]?.slug || "",
|
<Input
|
||||||
secretPath: "/"
|
{...field}
|
||||||
})
|
placeholder="My ST V3"
|
||||||
}
|
/>
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
</FormControl>
|
||||||
size="xs"
|
)}
|
||||||
>
|
/>
|
||||||
Add Scope
|
{tokenScopes.map(({ id }, index) => (
|
||||||
</Button>
|
<div className="flex items-end space-x-2 mb-3" key={id}>
|
||||||
</div>
|
<Controller
|
||||||
{tokenTrustedIps.map(({ id }, index) => (
|
control={control}
|
||||||
<div className="flex items-end space-x-2 mb-3" key={id}>
|
name={`scopes.${index}.permission`}
|
||||||
<Controller
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
control={control}
|
<FormControl
|
||||||
name={`trustedIps.${index}.ipAddress`}
|
className="mb-0"
|
||||||
defaultValue="0.0.0.0/0"
|
label={index === 0 ? "Permission" : undefined}
|
||||||
render={({ field, fieldState: { error } }) => {
|
errorText={error?.message}
|
||||||
return (
|
isError={Boolean(error)}
|
||||||
<FormControl
|
>
|
||||||
className="mb-0 flex-grow"
|
<Select
|
||||||
label={index === 0 ? "Trusted IP" : undefined}
|
defaultValue={field.value}
|
||||||
isError={Boolean(error)}
|
{...field}
|
||||||
errorText={error?.message}
|
onValueChange={(e) => onChange(e)}
|
||||||
|
className="w-36"
|
||||||
|
>
|
||||||
|
<SelectItem value="read" key="st-v3-read">
|
||||||
|
Read
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="readWrite" key="st-v3-write">
|
||||||
|
Read & Write
|
||||||
|
</SelectItem>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={`scopes.${index}.environment`}
|
||||||
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
className="mb-0"
|
||||||
|
label={index === 0 ? "Environment" : undefined}
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
defaultValue={field.value}
|
||||||
|
{...field}
|
||||||
|
onValueChange={(e) => onChange(e)}
|
||||||
|
className="w-36"
|
||||||
|
>
|
||||||
|
{currentWorkspace?.environments.map(({ name, slug }) => (
|
||||||
|
<SelectItem value={slug} key={slug}>
|
||||||
|
{name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={`scopes.${index}.secretPath`}
|
||||||
|
defaultValue="/"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
className="mb-0 flex-grow"
|
||||||
|
label={index === 0 ? "Secrets Path" : undefined}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input {...field} placeholder="can be /, /nested/**, /**/deep" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
onClick={() => remove(index)}
|
||||||
|
size="lg"
|
||||||
|
colorSchema="danger"
|
||||||
|
variant="plain"
|
||||||
|
ariaLabel="update"
|
||||||
|
className="p-3"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faXmark} />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div className="my-4 ml-1">
|
||||||
|
<Button
|
||||||
|
variant="outline_bg"
|
||||||
|
onClick={() =>
|
||||||
|
append({
|
||||||
|
permission: "read",
|
||||||
|
environment: currentWorkspace?.environments?.[0]?.slug || "",
|
||||||
|
secretPath: "/"
|
||||||
|
})
|
||||||
|
}
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
size="xs"
|
||||||
>
|
>
|
||||||
<Input
|
Add Scope
|
||||||
value={field.value}
|
</Button>
|
||||||
onChange={(e) => {
|
</div>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="expiresIn"
|
||||||
|
defaultValue=""
|
||||||
|
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label={`${popUp?.serviceTokenV3?.data ? "Update" : ""} Refresh Token Expires In`}
|
||||||
|
errorText={error?.message}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
className="mt-4"
|
||||||
|
>
|
||||||
|
<Select
|
||||||
|
defaultValue={field.value}
|
||||||
|
{...field}
|
||||||
|
onValueChange={(e) => onChange(e)}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
{expirations.map(({ label, value }) => (
|
||||||
|
<SelectItem value={String(value || "")} key={`api-key-expiration-${label}`}>
|
||||||
|
{label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
</TabPanel>
|
||||||
|
<TabPanel value={TabSections.Advanced}>
|
||||||
|
<div>
|
||||||
|
{tokenTrustedIps.map(({ id }, index) => (
|
||||||
|
<div className="flex items-end space-x-2 mb-3" key={id}>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={`trustedIps.${index}.ipAddress`}
|
||||||
|
defaultValue="0.0.0.0/0"
|
||||||
|
render={({ field, fieldState: { error } }) => {
|
||||||
|
return (
|
||||||
|
<FormControl
|
||||||
|
className="mb-0 flex-grow"
|
||||||
|
label={index === 0 ? "Trusted IP" : undefined}
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
value={field.value}
|
||||||
|
onChange={(e) => {
|
||||||
|
if (subscription?.ipAllowlisting) {
|
||||||
|
field.onChange(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePopUpOpen("upgradePlan");
|
||||||
|
}}
|
||||||
|
placeholder="123.456.789.0"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
onClick={() => {
|
||||||
|
if (subscription?.ipAllowlisting) {
|
||||||
|
removeTrustedIp(index);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
handlePopUpOpen("upgradePlan");
|
||||||
|
}}
|
||||||
|
size="lg"
|
||||||
|
colorSchema="danger"
|
||||||
|
variant="plain"
|
||||||
|
ariaLabel="update"
|
||||||
|
className="p-3"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faXmark} />
|
||||||
|
</IconButton>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div className="my-4 ml-1">
|
||||||
|
<Button
|
||||||
|
variant="outline_bg"
|
||||||
|
onClick={() => {
|
||||||
if (subscription?.ipAllowlisting) {
|
if (subscription?.ipAllowlisting) {
|
||||||
field.onChange(e);
|
appendTrustedIp({
|
||||||
|
ipAddress: "0.0.0.0/0"
|
||||||
|
})
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
handlePopUpOpen("upgradePlan");
|
handlePopUpOpen("upgradePlan");
|
||||||
}}
|
}}
|
||||||
placeholder="123.456.789.0"
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
|
size="xs"
|
||||||
|
>
|
||||||
|
Add IP Address
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
defaultValue="7200"
|
||||||
|
name="accessTokenTTL"
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
label="Access Token TTL (seconds)"
|
||||||
|
isError={Boolean(error)}
|
||||||
|
errorText={error?.message}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
{...field}
|
||||||
|
placeholder="7200"
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="mt-8">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="isRefreshTokenRotationEnabled"
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<Switch
|
||||||
|
id="label-refresh-token-rotation"
|
||||||
|
onCheckedChange={(isChecked) => onChange(isChecked)}
|
||||||
|
isChecked={value}
|
||||||
|
>
|
||||||
|
Refresh Token Rotation
|
||||||
|
</Switch>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
<p className="mt-4 text-sm font-normal text-mineshaft-400">When enabled, as a result of exchanging a refresh token, a new refresh token will be issued and the existing token will be invalidated.</p>
|
||||||
);
|
</div>
|
||||||
}}
|
</div>
|
||||||
/>
|
</TabPanel>
|
||||||
<IconButton
|
</Tabs>
|
||||||
onClick={() => {
|
<div className="flex items-center">
|
||||||
if (subscription?.ipAllowlisting) {
|
<Button
|
||||||
removeTrustedIp(index);
|
className="mr-4"
|
||||||
return;
|
size="sm"
|
||||||
}
|
type="submit"
|
||||||
|
isLoading={isSubmitting}
|
||||||
handlePopUpOpen("upgradePlan");
|
isDisabled={isSubmitting}
|
||||||
}}
|
|
||||||
size="lg"
|
|
||||||
colorSchema="danger"
|
|
||||||
variant="plain"
|
|
||||||
ariaLabel="update"
|
|
||||||
className="p-3"
|
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faXmark} />
|
{popUp?.serviceTokenV3?.data ? "Update" : "Create"}
|
||||||
</IconButton>
|
</Button>
|
||||||
|
<Button colorSchema="secondary" variant="plain">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
))}
|
</form>
|
||||||
<div className="my-4 ml-1">
|
) : (
|
||||||
<Button
|
<div className="mt-2 mb-3 mr-2 flex items-center justify-end rounded-md bg-white/[0.07] p-2 text-base text-gray-400">
|
||||||
variant="outline_bg"
|
<p className="mr-4 break-all">{newServiceTokenJSON}</p>
|
||||||
onClick={() => {
|
<IconButton
|
||||||
if (subscription?.ipAllowlisting) {
|
ariaLabel="copy icon"
|
||||||
appendTrustedIp({
|
colorSchema="secondary"
|
||||||
ipAddress: "0.0.0.0/0"
|
className="group relative"
|
||||||
})
|
onClick={copyTokenToClipboard}
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePopUpOpen("upgradePlan");
|
|
||||||
}}
|
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
|
||||||
size="xs"
|
|
||||||
>
|
>
|
||||||
Add IP Address
|
<FontAwesomeIcon icon={isServiceTokenJSONCopied ? faCheck : faCopy} />
|
||||||
</Button>
|
<span className="absolute -left-8 -top-20 hidden w-28 translate-y-full rounded-md bg-bunker-800 py-2 pl-3 text-center text-sm text-gray-400 group-hover:flex group-hover:animate-fadeIn">
|
||||||
|
Click to copy
|
||||||
|
</span>
|
||||||
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
<Controller
|
)}
|
||||||
control={control}
|
|
||||||
name="expiresIn"
|
|
||||||
defaultValue="15552000"
|
|
||||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
|
||||||
<FormControl
|
|
||||||
label={`${popUp?.serviceTokenV3?.data ? "Update" : ""} Expire In`}
|
|
||||||
errorText={error?.message}
|
|
||||||
isError={Boolean(error)}
|
|
||||||
className="mt-4"
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
defaultValue={field.value}
|
|
||||||
{...field}
|
|
||||||
onValueChange={(e) => onChange(e)}
|
|
||||||
className="w-full"
|
|
||||||
>
|
|
||||||
{expirations.map(({ label, value }) => (
|
|
||||||
<SelectItem value={String(value || "")} key={`api-key-expiration-${label}`}>
|
|
||||||
{label}
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</FormControl>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<div className="mt-8 flex items-center">
|
|
||||||
<Button
|
|
||||||
className="mr-4"
|
|
||||||
size="sm"
|
|
||||||
type="submit"
|
|
||||||
isLoading={isSubmitting}
|
|
||||||
isDisabled={isSubmitting}
|
|
||||||
>
|
|
||||||
{popUp?.serviceTokenV3?.data ? "Update" : "Create"}
|
|
||||||
</Button>
|
|
||||||
<Button colorSchema="secondary" variant="plain">
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
<UpgradePlanModal
|
<UpgradePlanModal
|
||||||
isOpen={popUp?.upgradePlan?.isOpen}
|
isOpen={popUp?.upgradePlan?.isOpen}
|
||||||
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
|
||||||
|
|||||||
+11
-9
@@ -34,6 +34,8 @@ type Props = {
|
|||||||
name?: string;
|
name?: string;
|
||||||
scopes?: ServiceTokenV3Scope[];
|
scopes?: ServiceTokenV3Scope[];
|
||||||
trustedIps?: ServiceTokenV3TrustedIp[];
|
trustedIps?: ServiceTokenV3TrustedIp[];
|
||||||
|
accessTokenTTL?: number;
|
||||||
|
isRefreshTokenRotationEnabled?: boolean;
|
||||||
}
|
}
|
||||||
) => void;
|
) => void;
|
||||||
};
|
};
|
||||||
@@ -81,10 +83,9 @@ export const ServiceTokenV3Table = ({
|
|||||||
<Th>Status</Th>
|
<Th>Status</Th>
|
||||||
<Th>Scopes</Th>
|
<Th>Scopes</Th>
|
||||||
<Th>Trusted IPs</Th>
|
<Th>Trusted IPs</Th>
|
||||||
{/* <Th># Times Used</Th> */}
|
<Th>Access Token TTL</Th>
|
||||||
<Th>Last Used</Th>
|
|
||||||
<Th>Created At</Th>
|
<Th>Created At</Th>
|
||||||
<Th>Expires At</Th>
|
<Th>Valid Until</Th>
|
||||||
<Th className="w-5" />
|
<Th className="w-5" />
|
||||||
</Tr>
|
</Tr>
|
||||||
</THead>
|
</THead>
|
||||||
@@ -97,12 +98,12 @@ export const ServiceTokenV3Table = ({
|
|||||||
_id,
|
_id,
|
||||||
name,
|
name,
|
||||||
isActive,
|
isActive,
|
||||||
lastUsed,
|
|
||||||
// usageCount,
|
|
||||||
scopes,
|
scopes,
|
||||||
trustedIps,
|
trustedIps,
|
||||||
createdAt,
|
createdAt,
|
||||||
expiresAt
|
expiresAt,
|
||||||
|
accessTokenTTL,
|
||||||
|
isRefreshTokenRotationEnabled
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<Tr className="h-10" key={`st-v3-${_id}`}>
|
<Tr className="h-10" key={`st-v3-${_id}`}>
|
||||||
@@ -160,8 +161,7 @@ export const ServiceTokenV3Table = ({
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</Td>
|
</Td>
|
||||||
{/* <Td>{usageCount}</Td> */}
|
<Td>{accessTokenTTL}</Td>
|
||||||
<Td>{lastUsed ? format(new Date(lastUsed), "yyyy-MM-dd") : "-"}</Td>
|
|
||||||
<Td>{format(new Date(createdAt), "yyyy-MM-dd")}</Td>
|
<Td>{format(new Date(createdAt), "yyyy-MM-dd")}</Td>
|
||||||
<Td>{expiresAt ? format(new Date(expiresAt), "yyyy-MM-dd") : "-"}</Td>
|
<Td>{expiresAt ? format(new Date(expiresAt), "yyyy-MM-dd") : "-"}</Td>
|
||||||
<Td className="flex justify-end">
|
<Td className="flex justify-end">
|
||||||
@@ -176,7 +176,9 @@ export const ServiceTokenV3Table = ({
|
|||||||
serviceTokenDataId: _id,
|
serviceTokenDataId: _id,
|
||||||
name,
|
name,
|
||||||
scopes,
|
scopes,
|
||||||
trustedIps
|
trustedIps,
|
||||||
|
accessTokenTTL,
|
||||||
|
isRefreshTokenRotationEnabled
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
size="lg"
|
size="lg"
|
||||||
|
|||||||
Reference in New Issue
Block a user