diff --git a/backend/src/controllers/v1/authController.ts b/backend/src/controllers/v1/authController.ts index e41f517ee..c27175bb1 100644 --- a/backend/src/controllers/v1/authController.ts +++ b/backend/src/controllers/v1/authController.ts @@ -1,21 +1,9 @@ import { Request, Response } from "express"; -import { Types } from "mongoose"; import jwt from "jsonwebtoken"; -import crypto from "crypto"; -import bcrypt from "bcrypt"; import * as bigintConversion from "bigint-conversion"; // eslint-disable-next-line @typescript-eslint/no-var-requires const jsrp = require("jsrp"); import { - IIdentity, - IIdentityTrustedIp, - IIdentityUniversalAuthClientSecret, - Identity, - IdentityAccessToken, - IdentityAuthMethod, - IdentityMembershipOrg, - IdentityUniversalAuth, - IdentityUniversalAuthClientSecret, LoginSRPDetail, TokenVersion, User @@ -25,30 +13,16 @@ import { checkUserDevice } from "../../helpers/user"; import { AuthTokenType } from "../../variables"; import { BadRequestError, - ForbiddenRequestError, - ResourceNotFoundError, UnauthorizedRequestError } from "../../utils/errors"; import { getAuthSecret, getHttpsEnabled, getJwtAuthLifetime, - getSaltRounds } from "../../config"; -import { ActorType, EventType, IRole } from "../../ee/models"; +import { ActorType } from "../../ee/models"; import { validateRequest } from "../../helpers/validation"; import * as reqValidator from "../../validation/auth"; -import { checkIPAgainstBlocklist, extractIPDetails, isValidIpOrCidr } from "../../utils/ip"; -import { getUserAgentType } from "../../utils/posthog"; -import { EEAuditLogService, EELicenseService } from "../../ee/services"; -import { - OrgPermissionActions, - OrgPermissionSubjects, - getAuthDataOrgPermissions, - getOrgRolePermissions, - isAtLeastAsPrivilegedOrg -} from "../../ee/services/RoleService"; -import { ForbiddenError } from "@casl/ability"; declare module "jsonwebtoken" { export interface AuthnJwtPayload extends jwt.JwtPayload { @@ -300,754 +274,4 @@ export const getNewToken = async (req: Request, res: Response) => { export const handleAuthProviderCallback = (req: Request, res: Response) => { res.redirect(`/login/provider/success?token=${encodeURIComponent(req.providerAuthToken)}`); -}; - -// ---- new IDENTITY logic - -const packageUniversalAuthClientSecretData = (identityUniversalAuthClientSecret: IIdentityUniversalAuthClientSecret) => ({ - _id: identityUniversalAuthClientSecret._id, - identityUniversalAuth: identityUniversalAuthClientSecret.identityUniversalAuth, - isClientSecretRevoked: identityUniversalAuthClientSecret.isClientSecretRevoked, - description: identityUniversalAuthClientSecret.description, - clientSecretPrefix: identityUniversalAuthClientSecret.clientSecretPrefix, - clientSecretNumUses: identityUniversalAuthClientSecret.clientSecretNumUses, - clientSecretNumUsesLimit: identityUniversalAuthClientSecret.clientSecretNumUsesLimit, - clientSecretTTL: identityUniversalAuthClientSecret.clientSecretTTL, - createdAt: identityUniversalAuthClientSecret.createdAt, - updatedAt: identityUniversalAuthClientSecret.updatedAt -}); - -/** - * Renews an access token by its TTL - * @param req - * @param res - */ - export const renewAccessToken = async (req: Request, res: Response) => { - const { - body: { - accessToken - } - } = await validateRequest(reqValidator.RenewAccessTokenV1, req); - - const decodedToken = ( - jwt.verify(accessToken, await getAuthSecret()) - ); - - if (decodedToken.authTokenType !== AuthTokenType.IDENTITY_ACCESS_TOKEN) throw UnauthorizedRequestError(); - - const identityAccessToken = await IdentityAccessToken.findOne({ - _id: decodedToken.identityAccessTokenId, - isAccessTokenRevoked: false - }); - - if (!identityAccessToken) throw UnauthorizedRequestError(); - - const { - accessTokenTTL, - accessTokenLastRenewedAt, - accessTokenMaxTTL, - createdAt: accessTokenCreatedAt - } = identityAccessToken; - - if (accessTokenTTL === accessTokenMaxTTL) throw UnauthorizedRequestError({ - message: "Failed to renew non-renewable access token" - }); - - // ttl check - if (accessTokenTTL > 0) { - const currentDate = new Date(); - if (accessTokenLastRenewedAt) { - // access token has been renewed - const accessTokenRenewed = new Date(accessTokenLastRenewedAt); - const ttlInMilliseconds = accessTokenTTL * 1000; - const expirationDate = new Date(accessTokenRenewed.getTime() + ttlInMilliseconds); - - if (currentDate > expirationDate) throw UnauthorizedRequestError({ - message: "Failed to renew MI access token due to TTL expiration" - }); - } else { - // access token has never been renewed - const accessTokenCreated = new Date(accessTokenCreatedAt); - const ttlInMilliseconds = accessTokenTTL * 1000; - const expirationDate = new Date(accessTokenCreated.getTime() + ttlInMilliseconds); - - if (currentDate > expirationDate) throw UnauthorizedRequestError({ - message: "Failed to renew MI access token due to TTL expiration" - }); - } - } - - // max ttl checks - if (accessTokenMaxTTL > 0) { - const accessTokenCreated = new Date(accessTokenCreatedAt); - const ttlInMilliseconds = accessTokenMaxTTL * 1000; - const currentDate = new Date(); - const expirationDate = new Date(accessTokenCreated.getTime() + ttlInMilliseconds); - - if (currentDate > expirationDate) throw UnauthorizedRequestError({ - message: "Failed to renew MI access token due to Max TTL expiration" - }); - - const extendToDate = new Date(currentDate.getTime() + accessTokenTTL); - if (extendToDate > expirationDate) throw UnauthorizedRequestError({ - message: "Failed to renew MI access token past its Max TTL expiration" - }); - } - - await IdentityAccessToken.findByIdAndUpdate( - identityAccessToken._id, - { - accessTokenLastRenewedAt: new Date() - } - ); - - return res.status(200).send({ - accessToken, - expiresIn: identityAccessToken.accessTokenTTL, - tokenType: "Bearer" - }); -} - -/** - * Return access token for identity with client id [clientId] - * and client secret [clientSecret] - * @param req - * @param res - */ - export const loginIdentityUniversalAuth = async (req: Request, res: Response) => { - const { - body: { - clientId, - clientSecret - } - } = await validateRequest(reqValidator.LoginUniversalAuthV1, req); - - const identityUniversalAuth = await IdentityUniversalAuth.findOne({ - clientId - }).populate<{ identity: IIdentity }>("identity"); - - if (!identityUniversalAuth) throw UnauthorizedRequestError(); - - checkIPAgainstBlocklist({ - ipAddress: req.realIP, - trustedIps: identityUniversalAuth.clientSecretTrustedIps - }); - - const clientSecretData = await IdentityUniversalAuthClientSecret.find({ - identity: identityUniversalAuth.identity, - isClientSecretRevoked: false - }); - - let validatedClientSecretDatum: IIdentityUniversalAuthClientSecret | undefined; - - for (const clientSecretDatum of clientSecretData) { - const isSecretValid = await bcrypt.compare( - clientSecret, - clientSecretDatum.clientSecretHash - ); - - if (isSecretValid) { - validatedClientSecretDatum = clientSecretDatum; - break; - } - } - - if (!validatedClientSecretDatum) throw UnauthorizedRequestError(); - - const { - clientSecretTTL, - clientSecretNumUses, - clientSecretNumUsesLimit, - } = validatedClientSecretDatum; - - if (clientSecretTTL > 0) { - const clientSecretCreated = new Date(validatedClientSecretDatum.createdAt) - const ttlInMilliseconds = clientSecretTTL * 1000; - const currentDate = new Date(); - const expirationTime = new Date(clientSecretCreated.getTime() + ttlInMilliseconds); - - if (currentDate > expirationTime) { - await IdentityUniversalAuthClientSecret.findByIdAndUpdate( - validatedClientSecretDatum._id, - { - isClientSecretRevoked: true - } - ); - - throw UnauthorizedRequestError({ - message: "Failed to authenticate identity credentials due to expired client secret" - }); - } - } - - if (clientSecretNumUsesLimit > 0 && clientSecretNumUses === clientSecretNumUsesLimit) { - // number of times client secret can be used for - // a login operation reached - await IdentityUniversalAuthClientSecret.findByIdAndUpdate( - validatedClientSecretDatum._id, - { - isClientSecretRevoked: true - }, - { - new: true - } - ); - - throw UnauthorizedRequestError({ - message: "Failed to authenticate identity credentials due to client secret number of uses limit reached" - }); - } - - // increment usage count by 1 - await IdentityUniversalAuthClientSecret - .findByIdAndUpdate( - validatedClientSecretDatum._id, - { - clientSecretLastUsedAt: new Date(), - $inc: { clientSecretNumUses: 1 } - }, - { - new: true - } - ); - - const identityAccessToken = await new IdentityAccessToken({ - identity: identityUniversalAuth.identity, - identityUniversalAuthClientSecret: validatedClientSecretDatum._id, - accessTokenNumUses: 0, - accessTokenNumUsesLimit: identityUniversalAuth.accessTokenNumUsesLimit, - accessTokenTTL: identityUniversalAuth.accessTokenTTL, - accessTokenMaxTTL: identityUniversalAuth.accessTokenMaxTTL, - accessTokenTrustedIps: identityUniversalAuth.accessTokenTrustedIps, - isAccessTokenRevoked: false - }).save(); - - // token version - const accessToken = createToken({ - payload: { - identityId: identityUniversalAuth.identity.toString(), - clientSecretId: validatedClientSecretDatum._id.toString(), - identityAccessTokenId: identityAccessToken._id.toString(), - authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN - }, - secret: await getAuthSecret() - }); - - const userAgent = req.headers["user-agent"] ?? ""; - - await EEAuditLogService.createAuditLog( - { - actor: { - type: ActorType.IDENTITY, - metadata: { - identityId: identityUniversalAuth.identity._id.toString(), - name: identityUniversalAuth.identity.name - } - }, - authPayload: identityUniversalAuth.identity, - ipAddress: req.realIP, - userAgent, - userAgentType: getUserAgentType(userAgent) - }, - { - type: EventType.LOGIN_IDENTITY_UNIVERSAL_AUTH, - metadata: { - identityId: identityUniversalAuth.identity._id.toString(), - clientSecretId: validatedClientSecretDatum._id.toString(), - identityAccessTokenId: identityAccessToken._id.toString() - } - } - ); - - return res.status(200).send({ - accessToken, - expiresIn: identityUniversalAuth.accessTokenTTL, - tokenType: "Bearer" - }); -} - -export const addIdentityUniversalAuth = async (req: Request, res: Response) => { - const { - params: { identityId }, - body: { - clientSecretTrustedIps, - accessTokenTTL, - accessTokenMaxTTL, - accessTokenNumUsesLimit, - accessTokenTrustedIps, - } - } = await validateRequest(reqValidator.AddUniversalAuthToIdentityV1, req); - - const identityMembershipOrg = await IdentityMembershipOrg - .findOne({ - identity: new Types.ObjectId(identityId) - }) - .populate<{ - identity: IIdentity, - customRole: IRole - }>("identity customRole"); - - if (!identityMembershipOrg) throw ResourceNotFoundError({ - message: `Failed to find identity with id ${identityId}` - }); - - if (identityMembershipOrg.identity?.authMethod) throw BadRequestError({ - message: "Failed to add universal auth to already-configured identity" - }); - - if (accessTokenMaxTTL > 0 && accessTokenTTL > accessTokenMaxTTL) { - throw BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }) - } - - const { permission } = await getAuthDataOrgPermissions({ - authData: req.authData, - organizationId: identityMembershipOrg.organization - }); - - ForbiddenError.from(permission).throwUnlessCan( - OrgPermissionActions.Create, - OrgPermissionSubjects.Identity - ); - - const plan = await EELicenseService.getPlan(identityMembershipOrg.organization); - - // validate trusted ips - const reformattedClientSecretTrustedIps = clientSecretTrustedIps.map((clientSecretTrustedIp) => { - if (!plan.ipAllowlisting && clientSecretTrustedIp.ipAddress !== "0.0.0.0/0") return res.status(400).send({ - message: "Failed to add IP access range to service token due to plan restriction. Upgrade plan to add IP access range." - }); - - const isValidIPOrCidr = isValidIpOrCidr(clientSecretTrustedIp.ipAddress); - - if (!isValidIPOrCidr) return res.status(400).send({ - message: "The IP is not a valid IPv4, IPv6, or CIDR block" - }); - - return extractIPDetails(clientSecretTrustedIp.ipAddress); - }); - - const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => { - if (!plan.ipAllowlisting && accessTokenTrustedIp.ipAddress !== "0.0.0.0/0") return res.status(400).send({ - message: "Failed to add IP access range to service token due to plan restriction. Upgrade plan to add IP access range." - }); - - const isValidIPOrCidr = isValidIpOrCidr(accessTokenTrustedIp.ipAddress); - - if (!isValidIPOrCidr) return res.status(400).send({ - message: "The IP is not a valid IPv4, IPv6, or CIDR block" - }); - - return extractIPDetails(accessTokenTrustedIp.ipAddress); - }); - - const identityUniversalAuth = await new IdentityUniversalAuth({ - identity: identityMembershipOrg.identity._id, - clientId: crypto.randomUUID(), - clientSecretTrustedIps: reformattedClientSecretTrustedIps, - accessTokenTTL, - accessTokenMaxTTL, - accessTokenNumUsesLimit, - accessTokenTrustedIps: reformattedAccessTokenTrustedIps, - }).save(); - - await Identity.findByIdAndUpdate( - identityMembershipOrg.identity._id, - { - authMethod: IdentityAuthMethod.UNIVERSAL_AUTH - } - ); - - await EEAuditLogService.createAuditLog( - req.authData, - { - type: EventType.ADD_IDENTITY_UNIVERSAL_AUTH, - metadata: { - identityId: identityMembershipOrg.identity._id.toString(), - clientSecretTrustedIps: reformattedClientSecretTrustedIps as Array, - accessTokenTTL, - accessTokenMaxTTL, - accessTokenNumUsesLimit, - accessTokenTrustedIps: reformattedAccessTokenTrustedIps as Array - } - } - ); - - return res.status(200).send({ - identityUniversalAuth - }); -} - -export const updateIdentityUniversalAuth = async (req: Request, res: Response) => { - const { - params: { identityId }, - body: { - clientSecretTrustedIps, - accessTokenTTL, // TODO: validate this and max TTL - accessTokenMaxTTL, - accessTokenNumUsesLimit, - accessTokenTrustedIps, - } - } = await validateRequest(reqValidator.UpdateUniversalAuthToIdentityV1, req); - - const identityMembershipOrg = await IdentityMembershipOrg - .findOne({ - identity: new Types.ObjectId(identityId) - }) - .populate<{ - identity: IIdentity, - customRole: IRole - }>("identity customRole"); - - if (!identityMembershipOrg) throw ResourceNotFoundError({ - message: `Failed to find identity with id ${identityId}` - }); - - if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.UNIVERSAL_AUTH) throw BadRequestError({ - message: "Failed to add universal auth to already-configured identity" - }); - - const { permission } = await getAuthDataOrgPermissions({ - authData: req.authData, - organizationId: identityMembershipOrg.organization - }); - - ForbiddenError.from(permission).throwUnlessCan( - OrgPermissionActions.Edit, - OrgPermissionSubjects.Identity - ); - - const plan = await EELicenseService.getPlan(identityMembershipOrg.organization); - - // validate trusted ips - let reformattedClientSecretTrustedIps; - if (clientSecretTrustedIps) { - reformattedClientSecretTrustedIps = clientSecretTrustedIps.map((clientSecretTrustedIp) => { - if (!plan.ipAllowlisting && clientSecretTrustedIp.ipAddress !== "0.0.0.0/0") return res.status(400).send({ - message: "Failed to add IP access range to service token due to plan restriction. Upgrade plan to add IP access range." - }); - - const isValidIPOrCidr = isValidIpOrCidr(clientSecretTrustedIp.ipAddress); - - if (!isValidIPOrCidr) return res.status(400).send({ - message: "The IP is not a valid IPv4, IPv6, or CIDR block" - }); - - return extractIPDetails(clientSecretTrustedIp.ipAddress); - }); - } - - let reformattedAccessTokenTrustedIps; - if (accessTokenTrustedIps) { - reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => { - if (!plan.ipAllowlisting && accessTokenTrustedIp.ipAddress !== "0.0.0.0/0") return res.status(400).send({ - message: "Failed to add IP access range to service token due to plan restriction. Upgrade plan to add IP access range." - }); - - const isValidIPOrCidr = isValidIpOrCidr(accessTokenTrustedIp.ipAddress); - - if (!isValidIPOrCidr) return res.status(400).send({ - message: "The IP is not a valid IPv4, IPv6, or CIDR block" - }); - - return extractIPDetails(accessTokenTrustedIp.ipAddress); - }); - } - - const identityUniversalAuth = await IdentityUniversalAuth.findOneAndUpdate( - { - identity: identityMembershipOrg.identity._id, - }, - { - clientSecretTrustedIps: reformattedClientSecretTrustedIps, - accessTokenTTL, - accessTokenMaxTTL, - accessTokenNumUsesLimit, - accessTokenTrustedIps: reformattedAccessTokenTrustedIps, - }, - { - new: true - } - ); - - await EEAuditLogService.createAuditLog( - req.authData, - { - type: EventType.UPDATE_IDENTITY_UNIVERSAL_AUTH, - metadata: { - identityId: identityMembershipOrg.identity._id.toString(), - clientSecretTrustedIps: reformattedClientSecretTrustedIps as Array, - accessTokenTTL, - accessTokenMaxTTL, - accessTokenNumUsesLimit, - accessTokenTrustedIps: reformattedAccessTokenTrustedIps as Array - } - } - ); - - return res.status(200).send({ - identityUniversalAuth - }); -} - -export const getIdentityUniversalAuth = async (req: Request, res: Response) => { - const { - params: { identityId } - } = await validateRequest(reqValidator.GetUniversalAuthForIdentityV1, req); - - const identityMembershipOrg = await IdentityMembershipOrg - .findOne({ - identity: new Types.ObjectId(identityId) - }) - .populate<{ - identity: IIdentity, - customRole: IRole - }>("identity customRole"); - - if (!identityMembershipOrg) throw ResourceNotFoundError({ - message: `Failed to find identity with id ${identityId}` - }); - - const { permission } = await getAuthDataOrgPermissions({ - authData: req.authData, - organizationId: identityMembershipOrg.organization - }); - - ForbiddenError.from(permission).throwUnlessCan( - OrgPermissionActions.Read, - OrgPermissionSubjects.Identity - ); - - if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.UNIVERSAL_AUTH) throw BadRequestError({ - message: "The identity does not have universal auth configured" - }); - - const identityUniversalAuth = await IdentityUniversalAuth.findOne({ - identity: identityMembershipOrg.identity._id, - }); - - await EEAuditLogService.createAuditLog( - req.authData, - { - type: EventType.GET_IDENTITY_UNIVERSAL_AUTH, - metadata: { - identityId: identityMembershipOrg.identity._id.toString(), - } - } - ); - - return res.status(200).send({ - identityUniversalAuth - }); -} - -export const createUniversalAuthClientSecret = async (req: Request, res: Response) => { - const { - params: { identityId }, - body: { - description, - numUsesLimit, - ttl - } - } = await validateRequest(reqValidator.CreateUniversalAuthClientSecretV1, req); - - const identityMembershipOrg = await IdentityMembershipOrg.findOne({ - identity: new Types.ObjectId(identityId) - }).populate<{ - identity: IIdentity, - customRole: IRole - }>("identity customRole"); - - if (!identityMembershipOrg) throw ResourceNotFoundError({ - message: `Failed to find identity with id ${identityId}` - }); - - const { permission } = await getAuthDataOrgPermissions({ - authData: req.authData, - organizationId: identityMembershipOrg.organization - }); - - ForbiddenError.from(permission).throwUnlessCan( - OrgPermissionActions.Create, - OrgPermissionSubjects.Identity - ); - - if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.UNIVERSAL_AUTH) throw BadRequestError({ - message: "The identity does not have universal auth configured" - }); - - const rolePermission = await getOrgRolePermissions( - identityMembershipOrg?.customRole?.slug ?? identityMembershipOrg.role, - identityMembershipOrg.organization.toString() - ); - const hasRequiredPrivileges = isAtLeastAsPrivilegedOrg(permission, rolePermission); - - if (!hasRequiredPrivileges) throw ForbiddenRequestError({ - message: "Failed to create client secret for more privileged identity" - }); - - const clientSecret = crypto.randomBytes(32).toString("hex"); - const clientSecretHash = await bcrypt.hash(clientSecret, await getSaltRounds()); - - const identityUniversalAuth = await IdentityUniversalAuth.findOne({ - identity: identityMembershipOrg.identity._id - }); - - if (!identityUniversalAuth) throw ResourceNotFoundError(); - - const identityUniversalAuthClientSecret = await new IdentityUniversalAuthClientSecret({ - identity: identityMembershipOrg.identity._id, - identityUniversalAuth: identityUniversalAuth._id, - description, - clientSecretPrefix: clientSecret.slice(0, 4), - clientSecretHash, - clientSecretNumUses: 0, - clientSecretNumUsesLimit: numUsesLimit, - clientSecretTTL: ttl, - isClientSecretRevoked: false - }).save(); - - await EEAuditLogService.createAuditLog( - req.authData, - { - type: EventType.CREATE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET, - metadata: { - identityId: identityMembershipOrg.identity._id.toString(), - clientSecretId: identityUniversalAuthClientSecret._id.toString() - } - } - ); - - return res.status(200).send({ - clientSecret, - clientSecretData: packageUniversalAuthClientSecretData(identityUniversalAuthClientSecret) - }); -} - -export const getUniversalAuthClientSecrets = async (req: Request, res: Response) => { - const { - params: { identityId } - } = await validateRequest(reqValidator.GetUniversalAuthClientSecretsV1, req); - - const identityMembershipOrg = await IdentityMembershipOrg.findOne({ - identity: new Types.ObjectId(identityId) - }).populate<{ - identity: IIdentity, - customRole: IRole - }>("identity customRole"); - - if (!identityMembershipOrg) throw ResourceNotFoundError(); - - const { permission } = await getAuthDataOrgPermissions({ - authData: req.authData, - organizationId: identityMembershipOrg.organization - }); - ForbiddenError.from(permission).throwUnlessCan( - OrgPermissionActions.Read, - OrgPermissionSubjects.Identity - ); - - if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.UNIVERSAL_AUTH) throw BadRequestError({ - message: "The identity does not have universal auth configured" - }); - - const rolePermission = await getOrgRolePermissions( - identityMembershipOrg?.customRole?.slug ?? identityMembershipOrg.role, - identityMembershipOrg.organization.toString() - ); - const hasRequiredPrivileges = isAtLeastAsPrivilegedOrg(permission, rolePermission); - - if (!hasRequiredPrivileges) throw ForbiddenRequestError({ - message: "Failed to get client secrets for more privileged MI" - }); - - const clientSecretData = await IdentityUniversalAuthClientSecret - .find({ - identity: identityMembershipOrg.identity, - isClientSecretRevoked: false - }) - .sort({ createdAt: -1 }) - .limit(5); - - await EEAuditLogService.createAuditLog( - req.authData, - { - type: EventType.GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRETS, - metadata: { - identityId: identityMembershipOrg.identity._id.toString() - } - } - ); - - return res.status(200).send({ - clientSecretData: clientSecretData.map((clientSecretDatum) => packageUniversalAuthClientSecretData(clientSecretDatum)) - }); -} - -export const revokeUniversalAuthClientSecret = async (req: Request, res: Response) => { - const { - params: { identityId, clientSecretId } - } = await validateRequest(reqValidator.RevokeUniversalAuthClientSecretV1, req); - - const identityMembershipOrg = await IdentityMembershipOrg - .findOne({ - identity: new Types.ObjectId(identityId) - }) - .populate<{ - identity: IIdentity, - customRole: IRole - }>("identity customRole"); - - if (!identityMembershipOrg) throw ResourceNotFoundError({ - message: `Failed to find identity with id ${identityId}` - }); - - const { permission } = await getAuthDataOrgPermissions({ - authData: req.authData, - organizationId: identityMembershipOrg.organization - }); - - ForbiddenError.from(permission).throwUnlessCan( - OrgPermissionActions.Delete, - OrgPermissionSubjects.Identity - ); - - const rolePermission = await getOrgRolePermissions( - identityMembershipOrg?.customRole?.slug ?? identityMembershipOrg.role, - identityMembershipOrg.organization.toString() - ); - const hasRequiredPrivileges = isAtLeastAsPrivilegedOrg(permission, rolePermission); - - if (!hasRequiredPrivileges) throw ForbiddenRequestError({ - message: "Failed to delete client secrets for more privileged identity" - }); - - const clientSecretData = await IdentityUniversalAuthClientSecret.findOneAndUpdate( - { - _id: new Types.ObjectId(clientSecretId), - identity: identityMembershipOrg.identity._id - }, - { - isClientSecretRevoked: true - }, - { - new: true - } - ); - - if (!clientSecretData) throw ResourceNotFoundError(); - - await EEAuditLogService.createAuditLog( - req.authData, - { - type: EventType.REVOKE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET, - metadata: { - identityId: identityMembershipOrg.identity._id.toString(), - clientSecretId: clientSecretId - } - } - ); - - return res.status(200).send({ - clientSecretData: packageUniversalAuthClientSecretData(clientSecretData) - }) -} \ No newline at end of file +}; \ No newline at end of file diff --git a/backend/src/controllers/v1/index.ts b/backend/src/controllers/v1/index.ts index fe171ae86..937936416 100644 --- a/backend/src/controllers/v1/index.ts +++ b/backend/src/controllers/v1/index.ts @@ -1,4 +1,5 @@ import * as authController from "./authController"; +import * as universalAuthController from "./universalAuthController"; import * as botController from "./botController"; import * as integrationAuthController from "./integrationAuthController"; import * as integrationController from "./integrationController"; @@ -20,6 +21,7 @@ import * as adminController from "./adminController"; export { authController, + universalAuthController, botController, integrationAuthController, integrationController, diff --git a/backend/src/controllers/v1/universalAuthController.ts b/backend/src/controllers/v1/universalAuthController.ts new file mode 100644 index 000000000..e22c4e686 --- /dev/null +++ b/backend/src/controllers/v1/universalAuthController.ts @@ -0,0 +1,791 @@ +import { Request, Response } from "express"; +import { Types } from "mongoose"; +import jwt from "jsonwebtoken"; +import crypto from "crypto"; +import bcrypt from "bcrypt"; +import { + IIdentity, + IIdentityTrustedIp, + IIdentityUniversalAuthClientSecret, + Identity, + IdentityAccessToken, + IdentityAuthMethod, + IdentityMembershipOrg, + IdentityUniversalAuth, + IdentityUniversalAuthClientSecret, +} from "../../models"; +import { createToken } from "../../helpers/auth"; +import { AuthTokenType } from "../../variables"; +import { + BadRequestError, + ForbiddenRequestError, + ResourceNotFoundError, + UnauthorizedRequestError +} from "../../utils/errors"; +import { + getAuthSecret, + getSaltRounds +} from "../../config"; +import { ActorType, EventType, IRole } from "../../ee/models"; +import { validateRequest } from "../../helpers/validation"; +import * as reqValidator from "../../validation/auth"; +import { checkIPAgainstBlocklist, extractIPDetails, isValidIpOrCidr } from "../../utils/ip"; +import { getUserAgentType } from "../../utils/posthog"; +import { EEAuditLogService, EELicenseService } from "../../ee/services"; +import { + OrgPermissionActions, + OrgPermissionSubjects, + getAuthDataOrgPermissions, + getOrgRolePermissions, + isAtLeastAsPrivilegedOrg +} from "../../ee/services/RoleService"; +import { ForbiddenError } from "@casl/ability"; + +const packageUniversalAuthClientSecretData = (identityUniversalAuthClientSecret: IIdentityUniversalAuthClientSecret) => ({ + _id: identityUniversalAuthClientSecret._id, + identityUniversalAuth: identityUniversalAuthClientSecret.identityUniversalAuth, + isClientSecretRevoked: identityUniversalAuthClientSecret.isClientSecretRevoked, + description: identityUniversalAuthClientSecret.description, + clientSecretPrefix: identityUniversalAuthClientSecret.clientSecretPrefix, + clientSecretNumUses: identityUniversalAuthClientSecret.clientSecretNumUses, + clientSecretNumUsesLimit: identityUniversalAuthClientSecret.clientSecretNumUsesLimit, + clientSecretTTL: identityUniversalAuthClientSecret.clientSecretTTL, + createdAt: identityUniversalAuthClientSecret.createdAt, + updatedAt: identityUniversalAuthClientSecret.updatedAt +}); + +/** + * Renews an access token by its TTL + * @param req + * @param res + */ +export const renewAccessToken = async (req: Request, res: Response) => { + const { + body: { + accessToken + } + } = await validateRequest(reqValidator.RenewAccessTokenV1, req); + + const decodedToken = ( + jwt.verify(accessToken, await getAuthSecret()) + ); + + if (decodedToken.authTokenType !== AuthTokenType.IDENTITY_ACCESS_TOKEN) throw UnauthorizedRequestError(); + + const identityAccessToken = await IdentityAccessToken.findOne({ + _id: decodedToken.identityAccessTokenId, + isAccessTokenRevoked: false + }); + + if (!identityAccessToken) throw UnauthorizedRequestError(); + + const { + accessTokenTTL, + accessTokenLastRenewedAt, + accessTokenMaxTTL, + createdAt: accessTokenCreatedAt + } = identityAccessToken; + + if (accessTokenTTL === accessTokenMaxTTL) throw UnauthorizedRequestError({ + message: "Failed to renew non-renewable access token" + }); + + // ttl check + if (accessTokenTTL > 0) { + const currentDate = new Date(); + if (accessTokenLastRenewedAt) { + // access token has been renewed + const accessTokenRenewed = new Date(accessTokenLastRenewedAt); + const ttlInMilliseconds = accessTokenTTL * 1000; + const expirationDate = new Date(accessTokenRenewed.getTime() + ttlInMilliseconds); + + if (currentDate > expirationDate) throw UnauthorizedRequestError({ + message: "Failed to renew MI access token due to TTL expiration" + }); + } else { + // access token has never been renewed + const accessTokenCreated = new Date(accessTokenCreatedAt); + const ttlInMilliseconds = accessTokenTTL * 1000; + const expirationDate = new Date(accessTokenCreated.getTime() + ttlInMilliseconds); + + if (currentDate > expirationDate) throw UnauthorizedRequestError({ + message: "Failed to renew MI access token due to TTL expiration" + }); + } + } + + // max ttl checks + if (accessTokenMaxTTL > 0) { + const accessTokenCreated = new Date(accessTokenCreatedAt); + const ttlInMilliseconds = accessTokenMaxTTL * 1000; + const currentDate = new Date(); + const expirationDate = new Date(accessTokenCreated.getTime() + ttlInMilliseconds); + + if (currentDate > expirationDate) throw UnauthorizedRequestError({ + message: "Failed to renew MI access token due to Max TTL expiration" + }); + + const extendToDate = new Date(currentDate.getTime() + accessTokenTTL); + if (extendToDate > expirationDate) throw UnauthorizedRequestError({ + message: "Failed to renew MI access token past its Max TTL expiration" + }); + } + + await IdentityAccessToken.findByIdAndUpdate( + identityAccessToken._id, + { + accessTokenLastRenewedAt: new Date() + } + ); + + return res.status(200).send({ + accessToken, + expiresIn: identityAccessToken.accessTokenTTL, + tokenType: "Bearer" + }); +} + +/** + * Return access token for identity with client id [clientId] + * and client secret [clientSecret] + * @param req + * @param res + */ +export const loginIdentityUniversalAuth = async (req: Request, res: Response) => { + const { + body: { + clientId, + clientSecret + } + } = await validateRequest(reqValidator.LoginUniversalAuthV1, req); + + const identityUniversalAuth = await IdentityUniversalAuth.findOne({ + clientId + }).populate<{ identity: IIdentity }>("identity"); + + if (!identityUniversalAuth) throw UnauthorizedRequestError(); + + checkIPAgainstBlocklist({ + ipAddress: req.realIP, + trustedIps: identityUniversalAuth.clientSecretTrustedIps + }); + + const clientSecretData = await IdentityUniversalAuthClientSecret.find({ + identity: identityUniversalAuth.identity, + isClientSecretRevoked: false + }); + + let validatedClientSecretDatum: IIdentityUniversalAuthClientSecret | undefined; + + for (const clientSecretDatum of clientSecretData) { + const isSecretValid = await bcrypt.compare( + clientSecret, + clientSecretDatum.clientSecretHash + ); + + if (isSecretValid) { + validatedClientSecretDatum = clientSecretDatum; + break; + } + } + + if (!validatedClientSecretDatum) throw UnauthorizedRequestError(); + + const { + clientSecretTTL, + clientSecretNumUses, + clientSecretNumUsesLimit, + } = validatedClientSecretDatum; + + if (clientSecretTTL > 0) { + const clientSecretCreated = new Date(validatedClientSecretDatum.createdAt) + const ttlInMilliseconds = clientSecretTTL * 1000; + const currentDate = new Date(); + const expirationTime = new Date(clientSecretCreated.getTime() + ttlInMilliseconds); + + if (currentDate > expirationTime) { + await IdentityUniversalAuthClientSecret.findByIdAndUpdate( + validatedClientSecretDatum._id, + { + isClientSecretRevoked: true + } + ); + + throw UnauthorizedRequestError({ + message: "Failed to authenticate identity credentials due to expired client secret" + }); + } + } + + if (clientSecretNumUsesLimit > 0 && clientSecretNumUses === clientSecretNumUsesLimit) { + // number of times client secret can be used for + // a login operation reached + await IdentityUniversalAuthClientSecret.findByIdAndUpdate( + validatedClientSecretDatum._id, + { + isClientSecretRevoked: true + }, + { + new: true + } + ); + + throw UnauthorizedRequestError({ + message: "Failed to authenticate identity credentials due to client secret number of uses limit reached" + }); + } + + // increment usage count by 1 + await IdentityUniversalAuthClientSecret + .findByIdAndUpdate( + validatedClientSecretDatum._id, + { + clientSecretLastUsedAt: new Date(), + $inc: { clientSecretNumUses: 1 } + }, + { + new: true + } + ); + + const identityAccessToken = await new IdentityAccessToken({ + identity: identityUniversalAuth.identity, + identityUniversalAuthClientSecret: validatedClientSecretDatum._id, + accessTokenNumUses: 0, + accessTokenNumUsesLimit: identityUniversalAuth.accessTokenNumUsesLimit, + accessTokenTTL: identityUniversalAuth.accessTokenTTL, + accessTokenMaxTTL: identityUniversalAuth.accessTokenMaxTTL, + accessTokenTrustedIps: identityUniversalAuth.accessTokenTrustedIps, + isAccessTokenRevoked: false + }).save(); + + // token version + const accessToken = createToken({ + payload: { + identityId: identityUniversalAuth.identity.toString(), + clientSecretId: validatedClientSecretDatum._id.toString(), + identityAccessTokenId: identityAccessToken._id.toString(), + authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN + }, + secret: await getAuthSecret() + }); + + const userAgent = req.headers["user-agent"] ?? ""; + + await EEAuditLogService.createAuditLog( + { + actor: { + type: ActorType.IDENTITY, + metadata: { + identityId: identityUniversalAuth.identity._id.toString(), + name: identityUniversalAuth.identity.name + } + }, + authPayload: identityUniversalAuth.identity, + ipAddress: req.realIP, + userAgent, + userAgentType: getUserAgentType(userAgent) + }, + { + type: EventType.LOGIN_IDENTITY_UNIVERSAL_AUTH, + metadata: { + identityId: identityUniversalAuth.identity._id.toString(), + identityUniversalAuthId: identityUniversalAuth._id.toString(), + clientSecretId: validatedClientSecretDatum._id.toString(), + identityAccessTokenId: identityAccessToken._id.toString() + } + } + ); + + return res.status(200).send({ + accessToken, + expiresIn: identityUniversalAuth.accessTokenTTL, + tokenType: "Bearer" + }); +} + +export const addIdentityUniversalAuth = async (req: Request, res: Response) => { + const { + params: { identityId }, + body: { + clientSecretTrustedIps, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps, + } + } = await validateRequest(reqValidator.AddUniversalAuthToIdentityV1, req); + + const identityMembershipOrg = await IdentityMembershipOrg + .findOne({ + identity: new Types.ObjectId(identityId) + }) + .populate<{ + identity: IIdentity, + customRole: IRole + }>("identity customRole"); + + if (!identityMembershipOrg) throw ResourceNotFoundError({ + message: `Failed to find identity with id ${identityId}` + }); + + if (identityMembershipOrg.identity?.authMethod) throw BadRequestError({ + message: "Failed to add universal auth to already-configured identity" + }); + + if (accessTokenMaxTTL > 0 && accessTokenTTL > accessTokenMaxTTL) { + throw BadRequestError({ message: "Access token TTL cannot be greater than max TTL" }) + } + + const { permission } = await getAuthDataOrgPermissions({ + authData: req.authData, + organizationId: identityMembershipOrg.organization + }); + + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Create, + OrgPermissionSubjects.Identity + ); + + const plan = await EELicenseService.getPlan(identityMembershipOrg.organization); + + // validate trusted ips + const reformattedClientSecretTrustedIps = clientSecretTrustedIps.map((clientSecretTrustedIp) => { + if (!plan.ipAllowlisting && clientSecretTrustedIp.ipAddress !== "0.0.0.0/0") return res.status(400).send({ + message: "Failed to add IP access range to service token due to plan restriction. Upgrade plan to add IP access range." + }); + + const isValidIPOrCidr = isValidIpOrCidr(clientSecretTrustedIp.ipAddress); + + if (!isValidIPOrCidr) return res.status(400).send({ + message: "The IP is not a valid IPv4, IPv6, or CIDR block" + }); + + return extractIPDetails(clientSecretTrustedIp.ipAddress); + }); + + const reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => { + if (!plan.ipAllowlisting && accessTokenTrustedIp.ipAddress !== "0.0.0.0/0") return res.status(400).send({ + message: "Failed to add IP access range to service token due to plan restriction. Upgrade plan to add IP access range." + }); + + const isValidIPOrCidr = isValidIpOrCidr(accessTokenTrustedIp.ipAddress); + + if (!isValidIPOrCidr) return res.status(400).send({ + message: "The IP is not a valid IPv4, IPv6, or CIDR block" + }); + + return extractIPDetails(accessTokenTrustedIp.ipAddress); + }); + + const identityUniversalAuth = await new IdentityUniversalAuth({ + identity: identityMembershipOrg.identity._id, + clientId: crypto.randomUUID(), + clientSecretTrustedIps: reformattedClientSecretTrustedIps, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps: reformattedAccessTokenTrustedIps, + }).save(); + + await Identity.findByIdAndUpdate( + identityMembershipOrg.identity._id, + { + authMethod: IdentityAuthMethod.UNIVERSAL_AUTH + } + ); + + await EEAuditLogService.createAuditLog( + req.authData, + { + type: EventType.ADD_IDENTITY_UNIVERSAL_AUTH, + metadata: { + identityId: identityMembershipOrg.identity._id.toString(), + clientSecretTrustedIps: reformattedClientSecretTrustedIps as Array, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps: reformattedAccessTokenTrustedIps as Array + } + } + ); + + return res.status(200).send({ + identityUniversalAuth + }); +} + +export const updateIdentityUniversalAuth = async (req: Request, res: Response) => { + const { + params: { identityId }, + body: { + clientSecretTrustedIps, + accessTokenTTL, // TODO: validate this and max TTL + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps, + } + } = await validateRequest(reqValidator.UpdateUniversalAuthToIdentityV1, req); + + const identityMembershipOrg = await IdentityMembershipOrg + .findOne({ + identity: new Types.ObjectId(identityId) + }) + .populate<{ + identity: IIdentity, + customRole: IRole + }>("identity customRole"); + + if (!identityMembershipOrg) throw ResourceNotFoundError({ + message: `Failed to find identity with id ${identityId}` + }); + + if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.UNIVERSAL_AUTH) throw BadRequestError({ + message: "Failed to add universal auth to already-configured identity" + }); + + const { permission } = await getAuthDataOrgPermissions({ + authData: req.authData, + organizationId: identityMembershipOrg.organization + }); + + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Edit, + OrgPermissionSubjects.Identity + ); + + const plan = await EELicenseService.getPlan(identityMembershipOrg.organization); + + // validate trusted ips + let reformattedClientSecretTrustedIps; + if (clientSecretTrustedIps) { + reformattedClientSecretTrustedIps = clientSecretTrustedIps.map((clientSecretTrustedIp) => { + if (!plan.ipAllowlisting && clientSecretTrustedIp.ipAddress !== "0.0.0.0/0") return res.status(400).send({ + message: "Failed to add IP access range to service token due to plan restriction. Upgrade plan to add IP access range." + }); + + const isValidIPOrCidr = isValidIpOrCidr(clientSecretTrustedIp.ipAddress); + + if (!isValidIPOrCidr) return res.status(400).send({ + message: "The IP is not a valid IPv4, IPv6, or CIDR block" + }); + + return extractIPDetails(clientSecretTrustedIp.ipAddress); + }); + } + + let reformattedAccessTokenTrustedIps; + if (accessTokenTrustedIps) { + reformattedAccessTokenTrustedIps = accessTokenTrustedIps.map((accessTokenTrustedIp) => { + if (!plan.ipAllowlisting && accessTokenTrustedIp.ipAddress !== "0.0.0.0/0") return res.status(400).send({ + message: "Failed to add IP access range to service token due to plan restriction. Upgrade plan to add IP access range." + }); + + const isValidIPOrCidr = isValidIpOrCidr(accessTokenTrustedIp.ipAddress); + + if (!isValidIPOrCidr) return res.status(400).send({ + message: "The IP is not a valid IPv4, IPv6, or CIDR block" + }); + + return extractIPDetails(accessTokenTrustedIp.ipAddress); + }); + } + + const identityUniversalAuth = await IdentityUniversalAuth.findOneAndUpdate( + { + identity: identityMembershipOrg.identity._id, + }, + { + clientSecretTrustedIps: reformattedClientSecretTrustedIps, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps: reformattedAccessTokenTrustedIps, + }, + { + new: true + } + ); + + await EEAuditLogService.createAuditLog( + req.authData, + { + type: EventType.UPDATE_IDENTITY_UNIVERSAL_AUTH, + metadata: { + identityId: identityMembershipOrg.identity._id.toString(), + clientSecretTrustedIps: reformattedClientSecretTrustedIps as Array, + accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit, + accessTokenTrustedIps: reformattedAccessTokenTrustedIps as Array + } + } + ); + + return res.status(200).send({ + identityUniversalAuth + }); +} + +export const getIdentityUniversalAuth = async (req: Request, res: Response) => { + const { + params: { identityId } + } = await validateRequest(reqValidator.GetUniversalAuthForIdentityV1, req); + + const identityMembershipOrg = await IdentityMembershipOrg + .findOne({ + identity: new Types.ObjectId(identityId) + }) + .populate<{ + identity: IIdentity, + customRole: IRole + }>("identity customRole"); + + if (!identityMembershipOrg) throw ResourceNotFoundError({ + message: `Failed to find identity with id ${identityId}` + }); + + const { permission } = await getAuthDataOrgPermissions({ + authData: req.authData, + organizationId: identityMembershipOrg.organization + }); + + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Read, + OrgPermissionSubjects.Identity + ); + + if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.UNIVERSAL_AUTH) throw BadRequestError({ + message: "The identity does not have universal auth configured" + }); + + const identityUniversalAuth = await IdentityUniversalAuth.findOne({ + identity: identityMembershipOrg.identity._id, + }); + + await EEAuditLogService.createAuditLog( + req.authData, + { + type: EventType.GET_IDENTITY_UNIVERSAL_AUTH, + metadata: { + identityId: identityMembershipOrg.identity._id.toString(), + } + } + ); + + return res.status(200).send({ + identityUniversalAuth + }); +} + +export const createUniversalAuthClientSecret = async (req: Request, res: Response) => { + const { + params: { identityId }, + body: { + description, + numUsesLimit, + ttl + } + } = await validateRequest(reqValidator.CreateUniversalAuthClientSecretV1, req); + + const identityMembershipOrg = await IdentityMembershipOrg.findOne({ + identity: new Types.ObjectId(identityId) + }).populate<{ + identity: IIdentity, + customRole: IRole + }>("identity customRole"); + + if (!identityMembershipOrg) throw ResourceNotFoundError({ + message: `Failed to find identity with id ${identityId}` + }); + + const { permission } = await getAuthDataOrgPermissions({ + authData: req.authData, + organizationId: identityMembershipOrg.organization + }); + + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Create, + OrgPermissionSubjects.Identity + ); + + if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.UNIVERSAL_AUTH) throw BadRequestError({ + message: "The identity does not have universal auth configured" + }); + + const rolePermission = await getOrgRolePermissions( + identityMembershipOrg?.customRole?.slug ?? identityMembershipOrg.role, + identityMembershipOrg.organization.toString() + ); + const hasRequiredPrivileges = isAtLeastAsPrivilegedOrg(permission, rolePermission); + + if (!hasRequiredPrivileges) throw ForbiddenRequestError({ + message: "Failed to create client secret for more privileged identity" + }); + + const clientSecret = crypto.randomBytes(32).toString("hex"); + const clientSecretHash = await bcrypt.hash(clientSecret, await getSaltRounds()); + + const identityUniversalAuth = await IdentityUniversalAuth.findOne({ + identity: identityMembershipOrg.identity._id + }); + + if (!identityUniversalAuth) throw ResourceNotFoundError(); + + const identityUniversalAuthClientSecret = await new IdentityUniversalAuthClientSecret({ + identity: identityMembershipOrg.identity._id, + identityUniversalAuth: identityUniversalAuth._id, + description, + clientSecretPrefix: clientSecret.slice(0, 4), + clientSecretHash, + clientSecretNumUses: 0, + clientSecretNumUsesLimit: numUsesLimit, + clientSecretTTL: ttl, + isClientSecretRevoked: false + }).save(); + + await EEAuditLogService.createAuditLog( + req.authData, + { + type: EventType.CREATE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET, + metadata: { + identityId: identityMembershipOrg.identity._id.toString(), + clientSecretId: identityUniversalAuthClientSecret._id.toString() + } + } + ); + + return res.status(200).send({ + clientSecret, + clientSecretData: packageUniversalAuthClientSecretData(identityUniversalAuthClientSecret) + }); +} + +export const getUniversalAuthClientSecrets = async (req: Request, res: Response) => { + const { + params: { identityId } + } = await validateRequest(reqValidator.GetUniversalAuthClientSecretsV1, req); + + const identityMembershipOrg = await IdentityMembershipOrg.findOne({ + identity: new Types.ObjectId(identityId) + }).populate<{ + identity: IIdentity, + customRole: IRole + }>("identity customRole"); + + if (!identityMembershipOrg) throw ResourceNotFoundError(); + + const { permission } = await getAuthDataOrgPermissions({ + authData: req.authData, + organizationId: identityMembershipOrg.organization + }); + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Read, + OrgPermissionSubjects.Identity + ); + + if (identityMembershipOrg.identity?.authMethod !== IdentityAuthMethod.UNIVERSAL_AUTH) throw BadRequestError({ + message: "The identity does not have universal auth configured" + }); + + const rolePermission = await getOrgRolePermissions( + identityMembershipOrg?.customRole?.slug ?? identityMembershipOrg.role, + identityMembershipOrg.organization.toString() + ); + const hasRequiredPrivileges = isAtLeastAsPrivilegedOrg(permission, rolePermission); + + if (!hasRequiredPrivileges) throw ForbiddenRequestError({ + message: "Failed to get client secrets for more privileged MI" + }); + + const clientSecretData = await IdentityUniversalAuthClientSecret + .find({ + identity: identityMembershipOrg.identity, + isClientSecretRevoked: false + }) + .sort({ createdAt: -1 }) + .limit(5); + + await EEAuditLogService.createAuditLog( + req.authData, + { + type: EventType.GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRETS, + metadata: { + identityId: identityMembershipOrg.identity._id.toString() + } + } + ); + + return res.status(200).send({ + clientSecretData: clientSecretData.map((clientSecretDatum) => packageUniversalAuthClientSecretData(clientSecretDatum)) + }); +} + +export const revokeUniversalAuthClientSecret = async (req: Request, res: Response) => { + const { + params: { identityId, clientSecretId } + } = await validateRequest(reqValidator.RevokeUniversalAuthClientSecretV1, req); + + const identityMembershipOrg = await IdentityMembershipOrg + .findOne({ + identity: new Types.ObjectId(identityId) + }) + .populate<{ + identity: IIdentity, + customRole: IRole + }>("identity customRole"); + + if (!identityMembershipOrg) throw ResourceNotFoundError({ + message: `Failed to find identity with id ${identityId}` + }); + + const { permission } = await getAuthDataOrgPermissions({ + authData: req.authData, + organizationId: identityMembershipOrg.organization + }); + + ForbiddenError.from(permission).throwUnlessCan( + OrgPermissionActions.Delete, + OrgPermissionSubjects.Identity + ); + + const rolePermission = await getOrgRolePermissions( + identityMembershipOrg?.customRole?.slug ?? identityMembershipOrg.role, + identityMembershipOrg.organization.toString() + ); + const hasRequiredPrivileges = isAtLeastAsPrivilegedOrg(permission, rolePermission); + + if (!hasRequiredPrivileges) throw ForbiddenRequestError({ + message: "Failed to delete client secrets for more privileged identity" + }); + + const clientSecretData = await IdentityUniversalAuthClientSecret.findOneAndUpdate( + { + _id: new Types.ObjectId(clientSecretId), + identity: identityMembershipOrg.identity._id + }, + { + isClientSecretRevoked: true + }, + { + new: true + } + ); + + if (!clientSecretData) throw ResourceNotFoundError(); + + await EEAuditLogService.createAuditLog( + req.authData, + { + type: EventType.REVOKE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET, + metadata: { + identityId: identityMembershipOrg.identity._id.toString(), + clientSecretId: clientSecretId + } + } + ); + + return res.status(200).send({ + clientSecretData: packageUniversalAuthClientSecretData(clientSecretData) + }) +} \ No newline at end of file diff --git a/backend/src/ee/models/auditLog/types.ts b/backend/src/ee/models/auditLog/types.ts index 3741d5ce7..a4e470414 100644 --- a/backend/src/ee/models/auditLog/types.ts +++ b/backend/src/ee/models/auditLog/types.ts @@ -248,6 +248,7 @@ interface LoginIdentityUniversalAuthEvent { type: EventType.LOGIN_IDENTITY_UNIVERSAL_AUTH ; metadata: { identityId: string; + identityUniversalAuthId: string; clientSecretId: string; identityAccessTokenId: string; }; diff --git a/backend/src/helpers/organization.ts b/backend/src/helpers/organization.ts index e591ef78a..36d1baeb6 100644 --- a/backend/src/helpers/organization.ts +++ b/backend/src/helpers/organization.ts @@ -7,6 +7,8 @@ import { Identity, IdentityMembership, IdentityMembershipOrg, + IdentityUniversalAuth, + IdentityUniversalAuthClientSecret, IncidentContactOrg, Integration, IntegrationAuth, @@ -124,8 +126,8 @@ export const deleteOrganization = async ({ await MembershipOrg.deleteMany({ organization: organization._id }); - - await Identity.deleteMany({ + + const identityIds = await IdentityMembershipOrg.distinct("identity", { organization: organization._id }); @@ -133,6 +135,24 @@ export const deleteOrganization = async ({ organization: organization._id }); + await Identity.deleteMany({ + _id: { + $in: identityIds + } + }); + + await IdentityUniversalAuth.deleteMany({ + identity: { + $in: identityIds + } + }); + + await IdentityUniversalAuthClientSecret.deleteMany({ + identity: { + $in: identityIds + } + }); + await BotOrg.deleteMany({ organization: organization._id }); diff --git a/backend/src/index.ts b/backend/src/index.ts index 9d5302f01..cdf2ac8e5 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -52,6 +52,7 @@ import { secretsFolder as v1SecretsFolder, serviceToken as v1ServiceTokenRouter, signup as v1SignupRouter, + universalAuth as v1UniversalAuthRouter, userAction as v1UserActionRouter, user as v1UserRouter, webhooks as v1WebhooksRouter, @@ -212,7 +213,8 @@ const main = async () => { // v1 routes app.use("/api/v1/signup", v1SignupRouter); - app.use("/api/v1/auth", v1AuthRouter); // note: updated for identities + app.use("/api/v1/auth", v1AuthRouter); + app.use("/api/v1/auth", v1UniversalAuthRouter); // new app.use("/api/v1/admin", v1AdminRouter); app.use("/api/v1/bot", v1BotRouter); app.use("/api/v1/user", v1UserRouter); diff --git a/backend/src/routes/v1/auth.ts b/backend/src/routes/v1/auth.ts index 93c9fe985..a3037f311 100644 --- a/backend/src/routes/v1/auth.ts +++ b/backend/src/routes/v1/auth.ts @@ -48,64 +48,4 @@ router.delete( authController.revokeAllSessions ); -// --- identity endpoints - -router.post( - "/token/renew", - authController.renewAccessToken -); - -router.post( - "/universal-auth/login", - authController.loginIdentityUniversalAuth -); - -router.post( - "/universal-auth/identities/:identityId", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT] - }), - authController.addIdentityUniversalAuth -); - -router.patch( - "/universal-auth/identities/:identityId", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT] - }), - authController.updateIdentityUniversalAuth -); - -router.get( - "/universal-auth/identities/:identityId", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT] - }), - authController.getIdentityUniversalAuth -); - -router.post( - "/universal-auth/identities/:identityId/client-secrets", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT] - }), - authController.createUniversalAuthClientSecret -); - -router.get( - "/universal-auth/identities/:identityId/client-secrets", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT] - }), - authController.getUniversalAuthClientSecrets -); - -router.delete( - "/universal-auth/identities/:identityId/client-secrets/:clientSecretId", - requireAuth({ - acceptedAuthModes: [AuthMode.JWT] - }), - authController.revokeUniversalAuthClientSecret -); - export default router; diff --git a/backend/src/routes/v1/index.ts b/backend/src/routes/v1/index.ts index c19ead569..8bc4642af 100644 --- a/backend/src/routes/v1/index.ts +++ b/backend/src/routes/v1/index.ts @@ -1,6 +1,7 @@ import signup from "./signup"; import bot from "./bot"; import auth from "./auth"; +import universalAuth from "./universalAuth"; import user from "./user"; import userAction from "./userAction"; import organization from "./organization"; @@ -23,6 +24,7 @@ import admin from "./admin"; export { signup, auth, + universalAuth, bot, user, userAction, diff --git a/backend/src/routes/v1/universalAuth.ts b/backend/src/routes/v1/universalAuth.ts new file mode 100644 index 000000000..d232f0ee8 --- /dev/null +++ b/backend/src/routes/v1/universalAuth.ts @@ -0,0 +1,66 @@ + +import express from "express"; +const router = express.Router(); +import { requireAuth } from "../../middleware"; +import { universalAuthController } from "../../controllers/v1"; +import { AuthMode } from "../../variables"; + +router.post( + "/token/renew", + universalAuthController.renewAccessToken +); + +router.post( + "/universal-auth/login", + universalAuthController.loginIdentityUniversalAuth +); + +router.post( + "/universal-auth/identities/:identityId", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + universalAuthController.addIdentityUniversalAuth +); + +router.patch( + "/universal-auth/identities/:identityId", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + universalAuthController.updateIdentityUniversalAuth +); + +router.get( + "/universal-auth/identities/:identityId", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + universalAuthController.getIdentityUniversalAuth +); + +router.post( + "/universal-auth/identities/:identityId/client-secrets", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + universalAuthController.createUniversalAuthClientSecret +); + +router.get( + "/universal-auth/identities/:identityId/client-secrets", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + universalAuthController.getUniversalAuthClientSecrets +); + +router.post( + "/universal-auth/identities/:identityId/client-secrets/:clientSecretId/revoke", + requireAuth({ + acceptedAuthModes: [AuthMode.JWT] + }), + universalAuthController.revokeUniversalAuthClientSecret +); + +export default router; \ No newline at end of file diff --git a/docs/documentation/platform/machine-identity.mdx b/docs/documentation/platform/identity.mdx similarity index 61% rename from docs/documentation/platform/machine-identity.mdx rename to docs/documentation/platform/identity.mdx index 57a82fb13..c9c85e78c 100644 --- a/docs/documentation/platform/machine-identity.mdx +++ b/docs/documentation/platform/identity.mdx @@ -1,51 +1,54 @@ --- -title: "Machine Identity" +title: Identity description: "Programmatically interact with Infisical" --- -A machine identity (MI) is an entity that you can create in Infisical. The MI represents a workload that wishes to access the Infisical API and comes with its own authentication credential. +A (machine) identity is an entity that you can create in Infisical. +Each identity represents a workload that wishes to access the Infisical API via an authentication method; this is similar to an IAM user in AWS or service account in GCP. -Similar to a user, a MI can be provisioned scoped access to resources at the organization or project-level. For instance, you may create a MI with scoped access to +An identity can be provisioned scoped access to resources at the organization or project-level via [role-based access controls (RBAC)](/documentation/platform/role-based-access-controls). For instance, you may create a identity with scoped access to fetch secrets back from the `/` path of the `development` environment in some project. - The MI feature is in beta. + The identity feature is in beta. - Currently, a MI can only be used to make authenticated requests to the Infisical API and does not work with any clients such as [Node SDK](https://github.com/Infisical/infisical-node) + Currently, an identity can only be used to make authenticated requests to the Infisical API and does not work with any clients such as [Node SDK](https://github.com/Infisical/infisical-node) , [Python SDK](https://github.com/Infisical/infisical-python), CLI, K8s operator, Terraform Provider, etc. We will be releasing compatibility with it across clients in the coming quarter. -Here's a few pointers to get you acquainted with MIs: +Each identity can be configured an authentication method. The only supported method at the moment is **Universal Auth (UA)** +which has the following properties: -- Each MI has a **Client ID** for which you can generate one or more **Client Secret(s)**. Together, a **Client ID** and **Client Secret** can be exchanged for an access token (i.e. login operation) to authenticate with the Infisical API. -- MIs support restrictions on the number of times that the **Client Secret(s)** and access token(s) can be used. -- MIs support token renewal that is the ability to extend the lifetime of a token by its TTL up to its maximum TTL since its creation. -- MIs support IP allowlisting; this means you can restrict the usage of **Client Secret(s)** and access token to a specific IP or CIDR range. -- MIs rely on the role-based permission system to provision access to resources like secrets. -- MIs support expiration, so, if specified, the client secret of the MI will automatically be defunct after a period of time. -- MIs tracks most recent usage of their client secrets and access tokens; they also keep track of each token's usage count. -- MIs are editable. +- In UA, each identity is assigned a **Client ID** for which you can generate one or more **Client Secret(s)**. Together, a **Client ID** and **Client Secret** can be exchanged for an access token (i.e. login operation) to authenticate with the Infisical API. +- UA supports restrictions on the number of times that the **Client Secret(s)** and access token(s) can be used. +- UA supports token renewal that is the ability to extend the lifetime of a token by its TTL up to its maximum TTL since its creation. +- UA supports IP allowlisting; this means you can restrict the usage of **Client Secret(s)** and access token to a specific IP or CIDR range. +- UA support expiration, so, if specified, the client secret of the identity will automatically be defunct after a period of time. +- UA tracks most recent usage of their client secrets and access tokens; it also keeps track of each token's usage count. -## Using machine identities +## Using identities -In the following steps, we explore how to create and use MIs for your applications to access the Infisical API. +In the following steps, we explore how to create and use identities for your applications to access the Infisical API. - - To create a machine identity, head to your Organization Settings > Access Control > Machine Identities and press **Create identity**. + + To create an identity, head to your Organization Settings > Access Control > Machine Identities and press **Create identity**. ![machine identities organization](../../images/platform/machine-identity/machine-identity-org.png) ![machine identities organization create](../../images/platform/machine-identity/machine-identity-org-create.png) - Now input a few details for your new MI; note that only the fields in the **General** tab are required. Here's some guidance for each field: + Now input a few details for your new identity. Here's some guidance for each field: + + - Name (required): A friendly name for the identity. + - Role (required): A role from the **Organization Roles** tab to permit the identity to access certain resources. + + Once you've created an identity, you'll be prompted to configure the **Universal Auth** authentication method for it. - - Name (required): A friendly name for the MI - - Role (required): A role from the **Organization Roles** tab to permit the MI to access certain resources. - - Access Token Max TTL (default is `7200`): The maximum lifetime for an acccess token in seconds; a value of `0` implies an infinite maximum lifetime. - Access Token TTL (default is `7200`): The incremental lifetime for an acccess token in seconds; a value of `0` implies an infinite incremental lifetime. + - Access Token Max TTL (default is `7200`): The maximum lifetime for an acccess token in seconds; a value of `0` implies an infinite maximum lifetime. - Access Token Max Number of Uses (default is `0`): The maximum number of times that an access token can be used; a value of `0` implies infinite number of uses. - Client Secret Trusted IPs: The IPs or CIDR ranges that the **Client Secret** can be used from together with the **Client ID** to get back an access token. By default, **Client Secrets** are given the `0.0.0.0/0` entry representing all possible IPv4 addresses. - Access Token Trusted IPs: The IPs or CIDR ranges that access tokens can be used from. By default, each token is given the `0.0.0.0/0` entry representing all possible IPv4 addresses. @@ -58,9 +61,9 @@ In the following steps, we explore how to create and use MIs for your applicatio - In order to use the MI, you'll need the non-sensitive **Client ID** - of the MI and a **Client Secret** for it; you can think of these credentials akin to a username - and password used to authenticate with the Infisical API. With that, press on the key icon on the MI to generate a **Client Secret** + In order to use the identity, you'll need the non-sensitive **Client ID** + of the identity and a **Client Secret** for it; you can think of these credentials akin to a username + and password used to authenticate with the Infisical API. With that, press on the key icon on the identity to generate a **Client Secret** for it. ![machine identities client secret create](../../images/platform/machine-identity/machine-identity-org-client-secret.png) @@ -73,26 +76,26 @@ In the following steps, we explore how to create and use MIs for your applicatio - TTL (default is `0`): The time-to-live for the **Client Secret**. By default, the TTL will be set to 0 which implies that the **Client Secret** will never expire; a value of `0` implies an infinite lifetime. - Max Number of Uses (default is `0`): The maximum number of times that the **Client Secret** can be used together with the **Client ID** to get back an access token; a value of `0` implies infinite number of uses. - - To enable the MI to access project-level resources such as secrets within a specific project, you should add it to that project. + + To enable the identity to access project-level resources such as secrets within a specific project, you should add it to that project. - To do this, head over to the project you want to add the MI to and go to Project Settings > Access Control > Machine Identities and press **Add identity**. + To do this, head over to the project you want to add the identity to and go to Project Settings > Access Control > Machine Identities and press **Add identity**. - Next, select the MI you want to add to the project and the role you want to assign it. + Next, select the identity you want to add to the project and the role you want to assign it. ![machine identities project](../../images/platform/machine-identity/machine-identity-project.png) ![machine identities project create](../../images/platform/machine-identity/machine-identity-project-create.png) - - To access the Infisical API as the MI, you should first perform a login operation + + To access the Infisical API as the identity, you should first perform a login operation that is to exchange the **Client ID** and **Client Secret** of the MI for an access token - by making a request to the `/api/v1/machine-identities/login` endpoint. + by making a request to the `/api/v1/auth/universal-auth/login` endpoint. #### Sample request ``` - curl --location --request POST 'https://app.infisical.com/api/v1/machine-identities/login' \ + curl --location --request POST 'https://app.infisical.com/api/v1/auth/universal-auth/login' \ --header 'Content-Type: application/x-www-form-urlencoded' \ --data-urlencode 'clientSecret=...' \ --data-urlencode 'clientId=...' @@ -111,10 +114,10 @@ In the following steps, we explore how to create and use MIs for your applicatio Next, you can use the access token to authenticate with the [Infisical API](/api-reference/overview/introduction) - Each MI access token has a time-to-live (TLL) which you can infer from the response of the login operation; - the default TTL is `7200` seconds which can be adjusted in the **Advanced** settings of the MI. + Each identity access token has a time-to-live (TLL) which you can infer from the response of the login operation; + the default TTL is `7200` seconds which can be adjusted. - If a MI access token expires, it can no longer authenticate with the Infisical API. In this case, + If an identity access token expires, it can no longer authenticate with the Infisical API. In this case, a new access token should be obtained from the aforementioned login operation. @@ -123,22 +126,22 @@ In the following steps, we explore how to create and use MIs for your applicatio **FAQ** - - A service token is a project-level authentication method that is being phased out in favor of MIs. + + A service token is a project-level authentication method that is being phased out in favor of identities. - Amongst many differences, MIs provide broader access over the Infisical API, utilizes the same role-based + Amongst many differences, identities provide broader access over the Infisical API, utilizes the same role-based permission system used by users, and comes with ample more configurable security measures. - + There are a few reasons for why this might happen: - The client secret or access token has expired. - - The MI is insufficently permissioned to interact with the resources you wish to access. + - The identity is insufficently permissioned to interact with the resources you wish to access. - You are attempting to access a `/raw` secrets endpoint that requires your project to disable E2EE. - The client secret/access token is being used from an untrusted IP. - A MI access token can have a time-to-live (TTL) or incremental lifetime afterwhich it expires. + A identity access token can have a time-to-live (TTL) or incremental lifetime afterwhich it expires. In certain cases, you may want to extend the lifespan of an access token; to do so, you must use the max TTL parameter. When TTL and max TTL are equal, a token is not renewable; when max TTL is greater than TTL, a token is renewable. @@ -146,12 +149,12 @@ In the following steps, we explore how to create and use MIs for your applicatio Note that the max TTL cannot be less than the TTL for an access token. - + There are a few reasons for why this might happen: - - You have insufficient organization permissions to create, read, update, delete machine identities. - - The MI you are trying to read, update, or delete is more privileged than yourself. - - The role you are trying to create a MI for or update a MI to is more privileged than yours. + - You have insufficient organization permissions to create, read, update, delete identities. + - The identity you are trying to read, update, or delete is more privileged than yourself. + - The role you are trying to create an identity for or update an identity to is more privileged than yours. 1. `/**`: This pattern matches all folders at any depth in the directory structure. For example, it would match folders like `/folder1/`, `/folder1/subfolder/`, and so on. diff --git a/docs/mint.json b/docs/mint.json index db9f9ec62..80f4d8226 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -118,7 +118,7 @@ "documentation/platform/pit-recovery", "documentation/platform/audit-logs", "documentation/platform/token", - "documentation/platform/machine-identity", + "documentation/platform/identity", "documentation/platform/mfa", "documentation/platform/pr-workflows", "documentation/platform/role-based-access-controls", diff --git a/frontend/src/hooks/api/auditLogs/types.tsx b/frontend/src/hooks/api/auditLogs/types.tsx index 63efda532..54f4882b3 100644 --- a/frontend/src/hooks/api/auditLogs/types.tsx +++ b/frontend/src/hooks/api/auditLogs/types.tsx @@ -221,6 +221,7 @@ interface LoginIdentityUniversalAuthEvent { type: EventType.LOGIN_IDENTITY_UNIVERSAL_AUTH ; metadata: { identityId: string; + identityUniversalAuthId: string; clientSecretId: string; identityAccessTokenId: string; }; diff --git a/frontend/src/hooks/api/identities/index.tsx b/frontend/src/hooks/api/identities/index.tsx index b893ff99d..61b3035eb 100644 --- a/frontend/src/hooks/api/identities/index.tsx +++ b/frontend/src/hooks/api/identities/index.tsx @@ -5,7 +5,7 @@ export { useCreateIdentity, useCreateIdentityUniversalAuthClientSecret, useDeleteIdentity, - useDeleteIdentityUniversalAuthClientSecret, + useRevokeIdentityUniversalAuthClientSecret, useUpdateIdentity, useUpdateIdentityUniversalAuth} from "./mutations"; export { diff --git a/frontend/src/hooks/api/identities/mutations.tsx b/frontend/src/hooks/api/identities/mutations.tsx index 044983f8f..3711fd459 100644 --- a/frontend/src/hooks/api/identities/mutations.tsx +++ b/frontend/src/hooks/api/identities/mutations.tsx @@ -6,6 +6,7 @@ import { organizationKeys } from "../organization/queries"; import { identitiesKeys } from "./queries"; import { AddIdentityUniversalAuthDTO, + ClientSecretData, CreateIdentityDTO, CreateIdentityUniversalAuthClientSecretDTO, CreateIdentityUniversalAuthClientSecretRes, @@ -146,15 +147,15 @@ export const useCreateIdentityUniversalAuthClientSecret = () => { }); }; -export const useDeleteIdentityUniversalAuthClientSecret = () => { +export const useRevokeIdentityUniversalAuthClientSecret = () => { const queryClient = useQueryClient(); - return useMutation({ + return useMutation({ mutationFn: async ({ identityId, clientSecretId }) => { - const { data: { identity } } = await apiRequest.delete(`/api/v1/auth/universal-auth/identities/${identityId}/client-secrets/${clientSecretId}`); - return identity; + const { data: { clientSecretData } } = await apiRequest.post<{ clientSecretData: ClientSecretData }>(`/api/v1/auth/universal-auth/identities/${identityId}/client-secrets/${clientSecretId}/revoke`); + return clientSecretData; }, onSuccess: (_, { identityId }) => { queryClient.invalidateQueries(identitiesKeys.getIdentityUniversalAuthClientSecrets(identityId)); diff --git a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityUniversalAuthClientSecretModal.tsx b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityUniversalAuthClientSecretModal.tsx index c6c276133..75f806698 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityUniversalAuthClientSecretModal.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityUniversalAuthClientSecretModal.tsx @@ -29,9 +29,8 @@ import { import { useToggle } from "@app/hooks"; import { useCreateIdentityUniversalAuthClientSecret, - useDeleteIdentityUniversalAuthClientSecret, useGetIdentityUniversalAuth, - useGetIdentityUniversalAuthClientSecrets} from "@app/hooks/api"; + useGetIdentityUniversalAuthClientSecrets, useRevokeIdentityUniversalAuthClientSecret} from "@app/hooks/api"; import { UsePopUpState } from "@app/hooks/usePopUp"; const schema = yup.object({ @@ -73,7 +72,7 @@ export const IdentityUniversalAuthClientSecretModal = ({ const { data: identityUniversalAuth } = useGetIdentityUniversalAuth(popUpData?.identityId ?? ""); const { mutateAsync: createClientSecretMutateAsync } = useCreateIdentityUniversalAuthClientSecret(); - const { mutateAsync: deleteClientSecretMutateAsync } = useDeleteIdentityUniversalAuthClientSecret(); + const { mutateAsync: revokeClientSecretMutateAsync } = useRevokeIdentityUniversalAuthClientSecret(); const { control, @@ -145,7 +144,7 @@ export const IdentityUniversalAuthClientSecretModal = ({ if (!popUpData?.identityId) return; - await deleteClientSecretMutateAsync({ + await revokeClientSecretMutateAsync({ identityId: popUpData.identityId, clientSecretId });