mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Resolve PR review items, moved identity auth logic into separate controller, etc.
This commit is contained in:
@@ -1,21 +1,9 @@
|
|||||||
import { Request, Response } from "express";
|
import { Request, Response } from "express";
|
||||||
import { Types } from "mongoose";
|
|
||||||
import jwt from "jsonwebtoken";
|
import jwt from "jsonwebtoken";
|
||||||
import crypto from "crypto";
|
|
||||||
import bcrypt from "bcrypt";
|
|
||||||
import * as bigintConversion from "bigint-conversion";
|
import * as bigintConversion from "bigint-conversion";
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
const jsrp = require("jsrp");
|
const jsrp = require("jsrp");
|
||||||
import {
|
import {
|
||||||
IIdentity,
|
|
||||||
IIdentityTrustedIp,
|
|
||||||
IIdentityUniversalAuthClientSecret,
|
|
||||||
Identity,
|
|
||||||
IdentityAccessToken,
|
|
||||||
IdentityAuthMethod,
|
|
||||||
IdentityMembershipOrg,
|
|
||||||
IdentityUniversalAuth,
|
|
||||||
IdentityUniversalAuthClientSecret,
|
|
||||||
LoginSRPDetail,
|
LoginSRPDetail,
|
||||||
TokenVersion,
|
TokenVersion,
|
||||||
User
|
User
|
||||||
@@ -25,30 +13,16 @@ import { checkUserDevice } from "../../helpers/user";
|
|||||||
import { AuthTokenType } from "../../variables";
|
import { AuthTokenType } from "../../variables";
|
||||||
import {
|
import {
|
||||||
BadRequestError,
|
BadRequestError,
|
||||||
ForbiddenRequestError,
|
|
||||||
ResourceNotFoundError,
|
|
||||||
UnauthorizedRequestError
|
UnauthorizedRequestError
|
||||||
} from "../../utils/errors";
|
} from "../../utils/errors";
|
||||||
import {
|
import {
|
||||||
getAuthSecret,
|
getAuthSecret,
|
||||||
getHttpsEnabled,
|
getHttpsEnabled,
|
||||||
getJwtAuthLifetime,
|
getJwtAuthLifetime,
|
||||||
getSaltRounds
|
|
||||||
} from "../../config";
|
} from "../../config";
|
||||||
import { ActorType, EventType, IRole } from "../../ee/models";
|
import { ActorType } from "../../ee/models";
|
||||||
import { validateRequest } from "../../helpers/validation";
|
import { validateRequest } from "../../helpers/validation";
|
||||||
import * as reqValidator from "../../validation/auth";
|
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" {
|
declare module "jsonwebtoken" {
|
||||||
export interface AuthnJwtPayload extends jwt.JwtPayload {
|
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) => {
|
export const handleAuthProviderCallback = (req: Request, res: Response) => {
|
||||||
res.redirect(`/login/provider/success?token=${encodeURIComponent(req.providerAuthToken)}`);
|
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.IdentityAccessTokenJwtPayload>(
|
|
||||||
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<IIdentityTrustedIp>,
|
|
||||||
accessTokenTTL,
|
|
||||||
accessTokenMaxTTL,
|
|
||||||
accessTokenNumUsesLimit,
|
|
||||||
accessTokenTrustedIps: reformattedAccessTokenTrustedIps as Array<IIdentityTrustedIp>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
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<IIdentityTrustedIp>,
|
|
||||||
accessTokenTTL,
|
|
||||||
accessTokenMaxTTL,
|
|
||||||
accessTokenNumUsesLimit,
|
|
||||||
accessTokenTrustedIps: reformattedAccessTokenTrustedIps as Array<IIdentityTrustedIp>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
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)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import * as authController from "./authController";
|
import * as authController from "./authController";
|
||||||
|
import * as universalAuthController from "./universalAuthController";
|
||||||
import * as botController from "./botController";
|
import * as botController from "./botController";
|
||||||
import * as integrationAuthController from "./integrationAuthController";
|
import * as integrationAuthController from "./integrationAuthController";
|
||||||
import * as integrationController from "./integrationController";
|
import * as integrationController from "./integrationController";
|
||||||
@@ -20,6 +21,7 @@ import * as adminController from "./adminController";
|
|||||||
|
|
||||||
export {
|
export {
|
||||||
authController,
|
authController,
|
||||||
|
universalAuthController,
|
||||||
botController,
|
botController,
|
||||||
integrationAuthController,
|
integrationAuthController,
|
||||||
integrationController,
|
integrationController,
|
||||||
|
|||||||
791
backend/src/controllers/v1/universalAuthController.ts
Normal file
791
backend/src/controllers/v1/universalAuthController.ts
Normal file
@@ -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.IdentityAccessTokenJwtPayload>(
|
||||||
|
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<IIdentityTrustedIp>,
|
||||||
|
accessTokenTTL,
|
||||||
|
accessTokenMaxTTL,
|
||||||
|
accessTokenNumUsesLimit,
|
||||||
|
accessTokenTrustedIps: reformattedAccessTokenTrustedIps as Array<IIdentityTrustedIp>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
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<IIdentityTrustedIp>,
|
||||||
|
accessTokenTTL,
|
||||||
|
accessTokenMaxTTL,
|
||||||
|
accessTokenNumUsesLimit,
|
||||||
|
accessTokenTrustedIps: reformattedAccessTokenTrustedIps as Array<IIdentityTrustedIp>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -248,6 +248,7 @@ interface LoginIdentityUniversalAuthEvent {
|
|||||||
type: EventType.LOGIN_IDENTITY_UNIVERSAL_AUTH ;
|
type: EventType.LOGIN_IDENTITY_UNIVERSAL_AUTH ;
|
||||||
metadata: {
|
metadata: {
|
||||||
identityId: string;
|
identityId: string;
|
||||||
|
identityUniversalAuthId: string;
|
||||||
clientSecretId: string;
|
clientSecretId: string;
|
||||||
identityAccessTokenId: string;
|
identityAccessTokenId: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import {
|
|||||||
Identity,
|
Identity,
|
||||||
IdentityMembership,
|
IdentityMembership,
|
||||||
IdentityMembershipOrg,
|
IdentityMembershipOrg,
|
||||||
|
IdentityUniversalAuth,
|
||||||
|
IdentityUniversalAuthClientSecret,
|
||||||
IncidentContactOrg,
|
IncidentContactOrg,
|
||||||
Integration,
|
Integration,
|
||||||
IntegrationAuth,
|
IntegrationAuth,
|
||||||
@@ -124,8 +126,8 @@ export const deleteOrganization = async ({
|
|||||||
await MembershipOrg.deleteMany({
|
await MembershipOrg.deleteMany({
|
||||||
organization: organization._id
|
organization: organization._id
|
||||||
});
|
});
|
||||||
|
|
||||||
await Identity.deleteMany({
|
const identityIds = await IdentityMembershipOrg.distinct("identity", {
|
||||||
organization: organization._id
|
organization: organization._id
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -133,6 +135,24 @@ export const deleteOrganization = async ({
|
|||||||
organization: organization._id
|
organization: organization._id
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await Identity.deleteMany({
|
||||||
|
_id: {
|
||||||
|
$in: identityIds
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await IdentityUniversalAuth.deleteMany({
|
||||||
|
identity: {
|
||||||
|
$in: identityIds
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await IdentityUniversalAuthClientSecret.deleteMany({
|
||||||
|
identity: {
|
||||||
|
$in: identityIds
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
await BotOrg.deleteMany({
|
await BotOrg.deleteMany({
|
||||||
organization: organization._id
|
organization: organization._id
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ import {
|
|||||||
secretsFolder as v1SecretsFolder,
|
secretsFolder as v1SecretsFolder,
|
||||||
serviceToken as v1ServiceTokenRouter,
|
serviceToken as v1ServiceTokenRouter,
|
||||||
signup as v1SignupRouter,
|
signup as v1SignupRouter,
|
||||||
|
universalAuth as v1UniversalAuthRouter,
|
||||||
userAction as v1UserActionRouter,
|
userAction as v1UserActionRouter,
|
||||||
user as v1UserRouter,
|
user as v1UserRouter,
|
||||||
webhooks as v1WebhooksRouter,
|
webhooks as v1WebhooksRouter,
|
||||||
@@ -212,7 +213,8 @@ const main = async () => {
|
|||||||
|
|
||||||
// v1 routes
|
// v1 routes
|
||||||
app.use("/api/v1/signup", v1SignupRouter);
|
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/admin", v1AdminRouter);
|
||||||
app.use("/api/v1/bot", v1BotRouter);
|
app.use("/api/v1/bot", v1BotRouter);
|
||||||
app.use("/api/v1/user", v1UserRouter);
|
app.use("/api/v1/user", v1UserRouter);
|
||||||
|
|||||||
@@ -48,64 +48,4 @@ router.delete(
|
|||||||
authController.revokeAllSessions
|
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;
|
export default router;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import signup from "./signup";
|
import signup from "./signup";
|
||||||
import bot from "./bot";
|
import bot from "./bot";
|
||||||
import auth from "./auth";
|
import auth from "./auth";
|
||||||
|
import universalAuth from "./universalAuth";
|
||||||
import user from "./user";
|
import user from "./user";
|
||||||
import userAction from "./userAction";
|
import userAction from "./userAction";
|
||||||
import organization from "./organization";
|
import organization from "./organization";
|
||||||
@@ -23,6 +24,7 @@ import admin from "./admin";
|
|||||||
export {
|
export {
|
||||||
signup,
|
signup,
|
||||||
auth,
|
auth,
|
||||||
|
universalAuth,
|
||||||
bot,
|
bot,
|
||||||
user,
|
user,
|
||||||
userAction,
|
userAction,
|
||||||
|
|||||||
66
backend/src/routes/v1/universalAuth.ts
Normal file
66
backend/src/routes/v1/universalAuth.ts
Normal file
@@ -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;
|
||||||
@@ -1,51 +1,54 @@
|
|||||||
---
|
---
|
||||||
title: "Machine Identity"
|
title: Identity
|
||||||
description: "Programmatically interact with Infisical"
|
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.
|
fetch secrets back from the `/` path of the `development` environment in some project.
|
||||||
|
|
||||||
<Note>
|
<Note>
|
||||||
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.
|
, [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.
|
We will be releasing compatibility with it across clients in the coming quarter.
|
||||||
</Note>
|
</Note>
|
||||||
|
|
||||||
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.
|
- 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.
|
||||||
- MIs support restrictions on the number of times that the **Client Secret(s)** and access token(s) can be used.
|
- UA supports 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.
|
- 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.
|
||||||
- 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.
|
- 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.
|
||||||
- MIs rely on the role-based permission system to provision access to resources like secrets.
|
- UA support expiration, so, if specified, the client secret of the identity will automatically be defunct after a period of time.
|
||||||
- MIs support expiration, so, if specified, the client secret of the MI 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.
|
||||||
- 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.
|
|
||||||
|
|
||||||
## 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.
|
||||||
|
|
||||||
<Steps>
|
<Steps>
|
||||||
<Step title="Creating a MI">
|
<Step title="Creating an identity">
|
||||||
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**.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
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 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.
|
- 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.
|
- 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.
|
- 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
|
|||||||
|
|
||||||
</Step>
|
</Step>
|
||||||
<Step title="Creating a Client Secret">
|
<Step title="Creating a Client Secret">
|
||||||
In order to use the MI, you'll need the non-sensitive **Client ID**
|
In order to use the identity, 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
|
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 MI to generate a **Client Secret**
|
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.
|
for it.
|
||||||
|
|
||||||

|

|
||||||
@@ -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.
|
- 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.
|
- 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.
|
||||||
</Step>
|
</Step>
|
||||||
<Step title="Adding a MI to a project">
|
<Step title="Adding an identity to a project">
|
||||||
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.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||

|

|
||||||
</Step>
|
</Step>
|
||||||
<Step title="Accessing the Infisical API with the MI">
|
<Step title="Accessing the Infisical API with the identity">
|
||||||
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
|
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
|
#### 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' \
|
--header 'Content-Type: application/x-www-form-urlencoded' \
|
||||||
--data-urlencode 'clientSecret=...' \
|
--data-urlencode 'clientSecret=...' \
|
||||||
--data-urlencode 'clientId=...'
|
--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)
|
Next, you can use the access token to authenticate with the [Infisical API](/api-reference/overview/introduction)
|
||||||
|
|
||||||
<Note>
|
<Note>
|
||||||
Each MI access token has a time-to-live (TLL) which you can infer from the response of the login operation;
|
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 in the **Advanced** settings of the MI.
|
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.
|
a new access token should be obtained from the aforementioned login operation.
|
||||||
</Note>
|
</Note>
|
||||||
</Step>
|
</Step>
|
||||||
@@ -123,22 +126,22 @@ In the following steps, we explore how to create and use MIs for your applicatio
|
|||||||
**FAQ**
|
**FAQ**
|
||||||
|
|
||||||
<AccordionGroup>
|
<AccordionGroup>
|
||||||
<Accordion title="What is the difference between a machine identity and service token?">
|
<Accordion title="What is the difference between an identity and service token?">
|
||||||
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.
|
permission system used by users, and comes with ample more configurable security measures.
|
||||||
</Accordion>
|
</Accordion>
|
||||||
<Accordion title="Why is the Infisical API rejecting my machine identity credentials?">
|
<Accordion title="Why is the Infisical API rejecting my identity credentials?">
|
||||||
There are a few reasons for why this might happen:
|
There are a few reasons for why this might happen:
|
||||||
|
|
||||||
- The client secret or access token has expired.
|
- 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.
|
- 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.
|
- The client secret/access token is being used from an untrusted IP.
|
||||||
</Accordion>
|
</Accordion>
|
||||||
<Accordion title="What is token renewal and TTL/Max TTL?">
|
<Accordion title="What is token renewal and TTL/Max TTL?">
|
||||||
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.
|
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.
|
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.
|
Note that the max TTL cannot be less than the TTL for an access token.
|
||||||
</Accordion>
|
</Accordion>
|
||||||
<Accordion title="Why can I not create, read, update, or delete a machine identity?">
|
<Accordion title="Why can I not create, read, update, or delete an identity?">
|
||||||
There are a few reasons for why this might happen:
|
There are a few reasons for why this might happen:
|
||||||
|
|
||||||
- You have insufficient organization permissions to create, read, update, delete machine identities.
|
- You have insufficient organization permissions to create, read, update, delete identities.
|
||||||
- The MI you are trying to read, update, or delete is more privileged than yourself.
|
- The identity 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.
|
- The role you are trying to create an identity for or update an identity to is more privileged than yours.
|
||||||
</Accordion>
|
</Accordion>
|
||||||
<Accordion title="Can you provide examples for using glob patterns?">
|
<Accordion title="Can you provide examples for using glob patterns?">
|
||||||
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.
|
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.
|
||||||
@@ -118,7 +118,7 @@
|
|||||||
"documentation/platform/pit-recovery",
|
"documentation/platform/pit-recovery",
|
||||||
"documentation/platform/audit-logs",
|
"documentation/platform/audit-logs",
|
||||||
"documentation/platform/token",
|
"documentation/platform/token",
|
||||||
"documentation/platform/machine-identity",
|
"documentation/platform/identity",
|
||||||
"documentation/platform/mfa",
|
"documentation/platform/mfa",
|
||||||
"documentation/platform/pr-workflows",
|
"documentation/platform/pr-workflows",
|
||||||
"documentation/platform/role-based-access-controls",
|
"documentation/platform/role-based-access-controls",
|
||||||
|
|||||||
@@ -221,6 +221,7 @@ interface LoginIdentityUniversalAuthEvent {
|
|||||||
type: EventType.LOGIN_IDENTITY_UNIVERSAL_AUTH ;
|
type: EventType.LOGIN_IDENTITY_UNIVERSAL_AUTH ;
|
||||||
metadata: {
|
metadata: {
|
||||||
identityId: string;
|
identityId: string;
|
||||||
|
identityUniversalAuthId: string;
|
||||||
clientSecretId: string;
|
clientSecretId: string;
|
||||||
identityAccessTokenId: string;
|
identityAccessTokenId: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ export {
|
|||||||
useCreateIdentity,
|
useCreateIdentity,
|
||||||
useCreateIdentityUniversalAuthClientSecret,
|
useCreateIdentityUniversalAuthClientSecret,
|
||||||
useDeleteIdentity,
|
useDeleteIdentity,
|
||||||
useDeleteIdentityUniversalAuthClientSecret,
|
useRevokeIdentityUniversalAuthClientSecret,
|
||||||
useUpdateIdentity,
|
useUpdateIdentity,
|
||||||
useUpdateIdentityUniversalAuth} from "./mutations";
|
useUpdateIdentityUniversalAuth} from "./mutations";
|
||||||
export {
|
export {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { organizationKeys } from "../organization/queries";
|
|||||||
import { identitiesKeys } from "./queries";
|
import { identitiesKeys } from "./queries";
|
||||||
import {
|
import {
|
||||||
AddIdentityUniversalAuthDTO,
|
AddIdentityUniversalAuthDTO,
|
||||||
|
ClientSecretData,
|
||||||
CreateIdentityDTO,
|
CreateIdentityDTO,
|
||||||
CreateIdentityUniversalAuthClientSecretDTO,
|
CreateIdentityUniversalAuthClientSecretDTO,
|
||||||
CreateIdentityUniversalAuthClientSecretRes,
|
CreateIdentityUniversalAuthClientSecretRes,
|
||||||
@@ -146,15 +147,15 @@ export const useCreateIdentityUniversalAuthClientSecret = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useDeleteIdentityUniversalAuthClientSecret = () => {
|
export const useRevokeIdentityUniversalAuthClientSecret = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation<Identity, {}, DeleteIdentityUniversalAuthClientSecretDTO>({
|
return useMutation<ClientSecretData, {}, DeleteIdentityUniversalAuthClientSecretDTO>({
|
||||||
mutationFn: async ({
|
mutationFn: async ({
|
||||||
identityId,
|
identityId,
|
||||||
clientSecretId
|
clientSecretId
|
||||||
}) => {
|
}) => {
|
||||||
const { data: { identity } } = await apiRequest.delete(`/api/v1/auth/universal-auth/identities/${identityId}/client-secrets/${clientSecretId}`);
|
const { data: { clientSecretData } } = await apiRequest.post<{ clientSecretData: ClientSecretData }>(`/api/v1/auth/universal-auth/identities/${identityId}/client-secrets/${clientSecretId}/revoke`);
|
||||||
return identity;
|
return clientSecretData;
|
||||||
},
|
},
|
||||||
onSuccess: (_, { identityId }) => {
|
onSuccess: (_, { identityId }) => {
|
||||||
queryClient.invalidateQueries(identitiesKeys.getIdentityUniversalAuthClientSecrets(identityId));
|
queryClient.invalidateQueries(identitiesKeys.getIdentityUniversalAuthClientSecrets(identityId));
|
||||||
|
|||||||
@@ -29,9 +29,8 @@ import {
|
|||||||
import { useToggle } from "@app/hooks";
|
import { useToggle } from "@app/hooks";
|
||||||
import {
|
import {
|
||||||
useCreateIdentityUniversalAuthClientSecret,
|
useCreateIdentityUniversalAuthClientSecret,
|
||||||
useDeleteIdentityUniversalAuthClientSecret,
|
|
||||||
useGetIdentityUniversalAuth,
|
useGetIdentityUniversalAuth,
|
||||||
useGetIdentityUniversalAuthClientSecrets} from "@app/hooks/api";
|
useGetIdentityUniversalAuthClientSecrets, useRevokeIdentityUniversalAuthClientSecret} from "@app/hooks/api";
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
const schema = yup.object({
|
const schema = yup.object({
|
||||||
@@ -73,7 +72,7 @@ export const IdentityUniversalAuthClientSecretModal = ({
|
|||||||
const { data: identityUniversalAuth } = useGetIdentityUniversalAuth(popUpData?.identityId ?? "");
|
const { data: identityUniversalAuth } = useGetIdentityUniversalAuth(popUpData?.identityId ?? "");
|
||||||
|
|
||||||
const { mutateAsync: createClientSecretMutateAsync } = useCreateIdentityUniversalAuthClientSecret();
|
const { mutateAsync: createClientSecretMutateAsync } = useCreateIdentityUniversalAuthClientSecret();
|
||||||
const { mutateAsync: deleteClientSecretMutateAsync } = useDeleteIdentityUniversalAuthClientSecret();
|
const { mutateAsync: revokeClientSecretMutateAsync } = useRevokeIdentityUniversalAuthClientSecret();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
control,
|
control,
|
||||||
@@ -145,7 +144,7 @@ export const IdentityUniversalAuthClientSecretModal = ({
|
|||||||
|
|
||||||
if (!popUpData?.identityId) return;
|
if (!popUpData?.identityId) return;
|
||||||
|
|
||||||
await deleteClientSecretMutateAsync({
|
await revokeClientSecretMutateAsync({
|
||||||
identityId: popUpData.identityId,
|
identityId: popUpData.identityId,
|
||||||
clientSecretId
|
clientSecretId
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user