mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
226 lines
8.2 KiB
TypeScript
226 lines
8.2 KiB
TypeScript
import { ForbiddenError } from "@casl/ability";
|
|
import jwt, { JwtPayload } from "jsonwebtoken";
|
|
|
|
import { TableName, TIdentityAccessTokens } from "@app/db/schemas";
|
|
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
|
import { getConfig } from "@app/lib/config/env";
|
|
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
|
|
import { checkIPAgainstBlocklist, TIp } from "@app/lib/ip";
|
|
|
|
import { AuthTokenType } from "../auth/auth-type";
|
|
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
|
|
import { TIdentityAccessTokenDALFactory } from "./identity-access-token-dal";
|
|
import {
|
|
TIdentityAccessTokenJwtPayload,
|
|
TRenewAccessTokenDTO,
|
|
TRevokeAccessTokenByIdDTO
|
|
} from "./identity-access-token-types";
|
|
|
|
type TIdentityAccessTokenServiceFactoryDep = {
|
|
identityAccessTokenDAL: TIdentityAccessTokenDALFactory;
|
|
identityOrgMembershipDAL: TIdentityOrgDALFactory;
|
|
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
|
};
|
|
|
|
export type TIdentityAccessTokenServiceFactory = ReturnType<typeof identityAccessTokenServiceFactory>;
|
|
|
|
export const identityAccessTokenServiceFactory = ({
|
|
identityAccessTokenDAL,
|
|
identityOrgMembershipDAL,
|
|
permissionService
|
|
}: TIdentityAccessTokenServiceFactoryDep) => {
|
|
const validateAccessTokenExp = async (identityAccessToken: TIdentityAccessTokens) => {
|
|
const {
|
|
id: tokenId,
|
|
accessTokenTTL,
|
|
accessTokenNumUses,
|
|
accessTokenNumUsesLimit,
|
|
accessTokenLastRenewedAt,
|
|
createdAt: accessTokenCreatedAt
|
|
} = identityAccessToken;
|
|
|
|
if (accessTokenNumUsesLimit > 0 && accessTokenNumUses > 0 && accessTokenNumUses >= accessTokenNumUsesLimit) {
|
|
await identityAccessTokenDAL.deleteById(tokenId);
|
|
throw new BadRequestError({
|
|
message: "Unable to renew because access token number of uses limit reached"
|
|
});
|
|
}
|
|
|
|
// ttl check
|
|
if (Number(accessTokenTTL) > 0) {
|
|
const currentDate = new Date();
|
|
if (accessTokenLastRenewedAt) {
|
|
// access token has been renewed
|
|
const accessTokenRenewed = new Date(accessTokenLastRenewedAt);
|
|
const ttlInMilliseconds = Number(accessTokenTTL) * 1000;
|
|
const expirationDate = new Date(accessTokenRenewed.getTime() + ttlInMilliseconds);
|
|
|
|
if (currentDate > expirationDate) {
|
|
await identityAccessTokenDAL.deleteById(tokenId);
|
|
throw new UnauthorizedError({
|
|
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 = Number(accessTokenTTL) * 1000;
|
|
const expirationDate = new Date(accessTokenCreated.getTime() + ttlInMilliseconds);
|
|
|
|
if (currentDate > expirationDate) {
|
|
await identityAccessTokenDAL.deleteById(tokenId);
|
|
throw new UnauthorizedError({
|
|
message: "Failed to renew MI access token due to TTL expiration"
|
|
});
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
const renewAccessToken = async ({ accessToken }: TRenewAccessTokenDTO) => {
|
|
const appCfg = getConfig();
|
|
|
|
const decodedToken = jwt.verify(accessToken, appCfg.AUTH_SECRET) as JwtPayload & {
|
|
identityAccessTokenId: string;
|
|
};
|
|
if (decodedToken.authTokenType !== AuthTokenType.IDENTITY_ACCESS_TOKEN) throw new UnauthorizedError();
|
|
|
|
const identityAccessToken = await identityAccessTokenDAL.findOne({
|
|
[`${TableName.IdentityAccessToken}.id` as "id"]: decodedToken.identityAccessTokenId,
|
|
isAccessTokenRevoked: false
|
|
});
|
|
if (!identityAccessToken) throw new UnauthorizedError();
|
|
|
|
await validateAccessTokenExp(identityAccessToken);
|
|
|
|
const { accessTokenMaxTTL, createdAt: accessTokenCreatedAt, accessTokenTTL } = identityAccessToken;
|
|
|
|
// max ttl checks - will it go above max ttl
|
|
if (Number(accessTokenMaxTTL) > 0) {
|
|
const accessTokenCreated = new Date(accessTokenCreatedAt);
|
|
const ttlInMilliseconds = Number(accessTokenMaxTTL) * 1000;
|
|
const currentDate = new Date();
|
|
const expirationDate = new Date(accessTokenCreated.getTime() + ttlInMilliseconds);
|
|
|
|
if (currentDate > expirationDate) {
|
|
await identityAccessTokenDAL.deleteById(identityAccessToken.id);
|
|
throw new UnauthorizedError({
|
|
message: "Failed to renew MI access token due to Max TTL expiration"
|
|
});
|
|
}
|
|
|
|
const extendToDate = new Date(currentDate.getTime() + Number(accessTokenTTL * 1000));
|
|
if (extendToDate > expirationDate) {
|
|
await identityAccessTokenDAL.deleteById(identityAccessToken.id);
|
|
throw new UnauthorizedError({
|
|
message: "Failed to renew MI access token past its Max TTL expiration"
|
|
});
|
|
}
|
|
}
|
|
|
|
const updatedIdentityAccessToken = await identityAccessTokenDAL.updateById(identityAccessToken.id, {
|
|
accessTokenLastRenewedAt: new Date()
|
|
});
|
|
|
|
return { accessToken, identityAccessToken: updatedIdentityAccessToken };
|
|
};
|
|
|
|
const revokeAccessToken = async (accessToken: string) => {
|
|
const appCfg = getConfig();
|
|
|
|
const decodedToken = jwt.verify(accessToken, appCfg.AUTH_SECRET) as JwtPayload & {
|
|
identityAccessTokenId: string;
|
|
};
|
|
if (decodedToken.authTokenType !== AuthTokenType.IDENTITY_ACCESS_TOKEN) throw new UnauthorizedError();
|
|
|
|
const identityAccessToken = await identityAccessTokenDAL.findOne({
|
|
[`${TableName.IdentityAccessToken}.id` as "id"]: decodedToken.identityAccessTokenId,
|
|
isAccessTokenRevoked: false
|
|
});
|
|
if (!identityAccessToken) throw new UnauthorizedError();
|
|
|
|
const revokedToken = await identityAccessTokenDAL.updateById(identityAccessToken.id, {
|
|
isAccessTokenRevoked: true
|
|
});
|
|
|
|
return { revokedToken };
|
|
};
|
|
|
|
const revokeAccessTokenById = async ({
|
|
tokenId,
|
|
actorId,
|
|
actor,
|
|
actorAuthMethod,
|
|
actorOrgId
|
|
}: TRevokeAccessTokenByIdDTO) => {
|
|
const identityAccessToken = await identityAccessTokenDAL.findOne({
|
|
[`${TableName.IdentityAccessToken}.id` as "id"]: tokenId,
|
|
isAccessTokenRevoked: false
|
|
});
|
|
if (!identityAccessToken) throw new UnauthorizedError();
|
|
|
|
const identityOrgMembership = await identityOrgMembershipDAL.findOne({
|
|
identityId: identityAccessToken.identityId
|
|
});
|
|
|
|
if (!identityOrgMembership) {
|
|
throw new UnauthorizedError({ message: "Identity does not belong to any organization" });
|
|
}
|
|
|
|
const { permission } = await permissionService.getOrgPermission(
|
|
actor,
|
|
actorId,
|
|
identityOrgMembership.orgId,
|
|
actorAuthMethod,
|
|
actorOrgId
|
|
);
|
|
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.Identity);
|
|
|
|
const revokedToken = await identityAccessTokenDAL.updateById(identityAccessToken.id, {
|
|
isAccessTokenRevoked: true
|
|
});
|
|
|
|
return { revokedToken };
|
|
};
|
|
|
|
const fnValidateIdentityAccessToken = async (token: TIdentityAccessTokenJwtPayload, ipAddress?: string) => {
|
|
const identityAccessToken = await identityAccessTokenDAL.findOne({
|
|
[`${TableName.IdentityAccessToken}.id` as "id"]: token.identityAccessTokenId,
|
|
isAccessTokenRevoked: false
|
|
});
|
|
if (!identityAccessToken) throw new UnauthorizedError();
|
|
if (identityAccessToken.isAccessTokenRevoked)
|
|
throw new UnauthorizedError({
|
|
message: "Failed to authorize revoked access token"
|
|
});
|
|
|
|
if (ipAddress && identityAccessToken) {
|
|
checkIPAgainstBlocklist({
|
|
ipAddress,
|
|
trustedIps: identityAccessToken?.accessTokenTrustedIps as TIp[]
|
|
});
|
|
}
|
|
|
|
const identityOrgMembership = await identityOrgMembershipDAL.findOne({
|
|
identityId: identityAccessToken.identityId
|
|
});
|
|
|
|
if (!identityOrgMembership) {
|
|
throw new UnauthorizedError({ message: "Identity does not belong to any organization" });
|
|
}
|
|
|
|
await validateAccessTokenExp(identityAccessToken);
|
|
|
|
await identityAccessTokenDAL.updateById(identityAccessToken.id, {
|
|
accessTokenLastUsedAt: new Date(),
|
|
$incr: {
|
|
accessTokenNumUses: 1
|
|
}
|
|
});
|
|
return { ...identityAccessToken, orgId: identityOrgMembership.orgId };
|
|
};
|
|
|
|
return { renewAccessToken, revokeAccessToken, revokeAccessTokenById, fnValidateIdentityAccessToken };
|
|
};
|