misc: add auth attempt metrics

This commit is contained in:
Sheen Capadngan
2025-10-30 02:05:31 +08:00
parent e40031aeb5
commit 5d83101462
16 changed files with 1701 additions and 1298 deletions

View File

@@ -157,26 +157,30 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
metadata: userMetadata
});
authAttemptCounter.add(1, {
"infisical.user.email": email.toLowerCase(),
"infisical.user.id": user.id,
"infisical.organization.id": organization.id,
"infisical.organization.name": organization.name,
"infisical.auth.method": AuthAttemptAuthMethod.SAML,
"infisical.auth.result": AuthAttemptAuthResult.SUCCESS,
"client.address": requestContext.get("ip"),
"user_agent.original": requestContext.get("userAgent")
});
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
authAttemptCounter.add(1, {
"infisical.user.email": email.toLowerCase(),
"infisical.user.id": user.id,
"infisical.organization.id": organization.id,
"infisical.organization.name": organization.name,
"infisical.auth.method": AuthAttemptAuthMethod.SAML,
"infisical.auth.result": AuthAttemptAuthResult.SUCCESS,
"client.address": requestContext.get("ip"),
"user_agent.original": requestContext.get("userAgent")
});
}
cb(null, { isUserCompleted, providerAuthToken });
} catch (error) {
authAttemptCounter.add(1, {
"infisical.user.email": email.toLowerCase(),
"infisical.auth.method": AuthAttemptAuthMethod.SAML,
"infisical.auth.result": AuthAttemptAuthResult.FAILURE,
"client.address": requestContext.get("ip"),
"user_agent.original": requestContext.get("userAgent")
});
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
authAttemptCounter.add(1, {
"infisical.user.email": email.toLowerCase(),
"infisical.auth.method": AuthAttemptAuthMethod.SAML,
"infisical.auth.result": AuthAttemptAuthResult.FAILURE,
"client.address": requestContext.get("ip"),
"user_agent.original": requestContext.get("userAgent")
});
}
logger.error(error);
cb(error as Error);

View File

@@ -757,29 +757,33 @@ export const oidcConfigServiceFactory = ({
manageGroupMemberships: oidcCfg.manageGroupMemberships
})
.then(({ isUserCompleted, providerAuthToken, user }) => {
authAttemptCounter.add(1, {
"infisical.user.email": claims?.email?.toLowerCase(),
"infisical.user.id": user.id,
"infisical.organization.id": org.id,
"infisical.organization.name": org.name,
"infisical.auth.method": AuthAttemptAuthMethod.OIDC,
"infisical.auth.result": AuthAttemptAuthResult.SUCCESS,
"client.address": requestContext.get("ip"),
"user_agent.original": requestContext.get("userAgent")
});
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
authAttemptCounter.add(1, {
"infisical.user.email": claims?.email?.toLowerCase(),
"infisical.user.id": user.id,
"infisical.organization.id": org.id,
"infisical.organization.name": org.name,
"infisical.auth.method": AuthAttemptAuthMethod.OIDC,
"infisical.auth.result": AuthAttemptAuthResult.SUCCESS,
"client.address": requestContext.get("ip"),
"user_agent.original": requestContext.get("userAgent")
});
}
cb(null, { isUserCompleted, providerAuthToken });
})
.catch((error) => {
authAttemptCounter.add(1, {
"infisical.user.email": claims?.email?.toLowerCase(),
"infisical.organization.id": org.id,
"infisical.organization.name": org.name,
"infisical.auth.method": AuthAttemptAuthMethod.OIDC,
"infisical.auth.result": AuthAttemptAuthResult.FAILURE,
"client.address": requestContext.get("ip"),
"user_agent.original": requestContext.get("userAgent")
});
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
authAttemptCounter.add(1, {
"infisical.user.email": claims?.email?.toLowerCase(),
"infisical.organization.id": org.id,
"infisical.organization.name": org.name,
"infisical.auth.method": AuthAttemptAuthMethod.OIDC,
"infisical.auth.result": AuthAttemptAuthResult.FAILURE,
"client.address": requestContext.get("ip"),
"user_agent.original": requestContext.get("userAgent")
});
}
cb(error);
});

View File

@@ -8,7 +8,19 @@ export enum AuthAttemptAuthMethod {
OIDC = "oidc",
GOOGLE = "google",
GITHUB = "github",
GITLAB = "gitlab"
GITLAB = "gitlab",
TOKEN_AUTH = "token-auth",
UNIVERSAL_AUTH = "universal-auth",
KUBERNETES_AUTH = "kubernetes-auth",
GCP_AUTH = "gcp-auth",
ALICLOUD_AUTH = "alicloud-auth",
AWS_AUTH = "aws-auth",
AZURE_AUTH = "azure-auth",
TLS_CERT_AUTH = "tls-cert-auth",
OCI_AUTH = "oci-auth",
OIDC_AUTH = "oidc-auth",
JWT_AUTH = "jwt-auth",
LDAP_AUTH = "ldap-auth"
}
export enum AuthAttemptAuthResult {

View File

@@ -76,28 +76,31 @@ export const registerOauthMiddlewares = (server: FastifyZodProvider) => {
orgSlug
});
authAttemptCounter.add(1, {
"infisical.user.email": email,
"infisical.user.id": user.id,
"infisical.organization.id": orgId,
"infisical.organization.name": orgName,
"infisical.auth.method": AuthAttemptAuthMethod.GOOGLE,
"infisical.auth.result": AuthAttemptAuthResult.SUCCESS,
"client.address": requestContext.get("ip"),
"user_agent.original": requestContext.get("userAgent")
});
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
authAttemptCounter.add(1, {
"infisical.user.email": email,
"infisical.user.id": user.id,
"infisical.organization.id": orgId,
"infisical.organization.name": orgName,
"infisical.auth.method": AuthAttemptAuthMethod.GOOGLE,
"infisical.auth.result": AuthAttemptAuthResult.SUCCESS,
"client.address": requestContext.get("ip"),
"user_agent.original": requestContext.get("userAgent")
});
}
cb(null, { isUserCompleted, providerAuthToken });
} catch (error) {
logger.error(error);
authAttemptCounter.add(1, {
"infisical.user.email": email,
"infisical.auth.method": AuthAttemptAuthMethod.GOOGLE,
"infisical.auth.result": AuthAttemptAuthResult.FAILURE,
"client.address": requestContext.get("ip"),
"user_agent.original": requestContext.get("userAgent")
});
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
authAttemptCounter.add(1, {
"infisical.user.email": email,
"infisical.auth.method": AuthAttemptAuthMethod.GOOGLE,
"infisical.auth.result": AuthAttemptAuthResult.FAILURE,
"client.address": requestContext.get("ip"),
"user_agent.original": requestContext.get("userAgent")
});
}
cb(error as Error, false);
}
}
@@ -144,27 +147,30 @@ export const registerOauthMiddlewares = (server: FastifyZodProvider) => {
callbackPort
});
authAttemptCounter.add(1, {
"infisical.user.email": email,
"infisical.user.id": user.id,
"infisical.organization.id": orgId,
"infisical.organization.name": orgName,
"infisical.auth.method": AuthAttemptAuthMethod.GITHUB,
"infisical.auth.result": AuthAttemptAuthResult.SUCCESS,
"client.address": requestContext.get("ip"),
"user_agent.original": requestContext.get("userAgent")
});
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
authAttemptCounter.add(1, {
"infisical.user.email": email,
"infisical.user.id": user.id,
"infisical.organization.id": orgId,
"infisical.organization.name": orgName,
"infisical.auth.method": AuthAttemptAuthMethod.GITHUB,
"infisical.auth.result": AuthAttemptAuthResult.SUCCESS,
"client.address": requestContext.get("ip"),
"user_agent.original": requestContext.get("userAgent")
});
}
done(null, { isUserCompleted, providerAuthToken, externalProviderAccessToken: accessToken });
} catch (err) {
authAttemptCounter.add(1, {
"infisical.user.email": email,
"infisical.auth.method": AuthAttemptAuthMethod.GITHUB,
"infisical.auth.result": AuthAttemptAuthResult.FAILURE,
"client.address": requestContext.get("ip"),
"user_agent.original": requestContext.get("userAgent")
});
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
authAttemptCounter.add(1, {
"infisical.user.email": email,
"infisical.auth.method": AuthAttemptAuthMethod.GITHUB,
"infisical.auth.result": AuthAttemptAuthResult.FAILURE,
"client.address": requestContext.get("ip"),
"user_agent.original": requestContext.get("userAgent")
});
}
logger.error(err);
done(err as Error, false);
}
@@ -204,26 +210,30 @@ export const registerOauthMiddlewares = (server: FastifyZodProvider) => {
callbackPort
});
authAttemptCounter.add(1, {
"infisical.user.email": email,
"infisical.user.id": user.id,
"infisical.organization.id": orgId,
"infisical.organization.name": orgName,
"infisical.auth.method": AuthAttemptAuthMethod.GITLAB,
"infisical.auth.result": AuthAttemptAuthResult.SUCCESS,
"client.address": requestContext.get("ip"),
"user_agent.original": requestContext.get("userAgent")
});
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
authAttemptCounter.add(1, {
"infisical.user.email": email,
"infisical.user.id": user.id,
"infisical.organization.id": orgId,
"infisical.organization.name": orgName,
"infisical.auth.method": AuthAttemptAuthMethod.GITLAB,
"infisical.auth.result": AuthAttemptAuthResult.SUCCESS,
"client.address": requestContext.get("ip"),
"user_agent.original": requestContext.get("userAgent")
});
}
return cb(null, { isUserCompleted, providerAuthToken });
} catch (error) {
authAttemptCounter.add(1, {
"infisical.user.email": email,
"infisical.auth.method": AuthAttemptAuthMethod.GITLAB,
"infisical.auth.result": AuthAttemptAuthResult.FAILURE,
"client.address": requestContext.get("ip"),
"user_agent.original": requestContext.get("userAgent")
});
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
authAttemptCounter.add(1, {
"infisical.user.email": email,
"infisical.auth.method": AuthAttemptAuthMethod.GITLAB,
"infisical.auth.result": AuthAttemptAuthResult.FAILURE,
"client.address": requestContext.get("ip"),
"user_agent.original": requestContext.get("userAgent")
});
}
logger.error(error);
cb(error as Error, false);

View File

@@ -386,6 +386,8 @@ export const authLoginServiceFactory = ({
providerAuthToken?: string;
captchaToken?: string;
}) => {
const appCfg = getConfig();
try {
const usersByUsername = await userDAL.findUserEncKeyByUsername({
username: email
@@ -440,15 +442,17 @@ export const authLoginServiceFactory = ({
organizationId
});
authAttemptCounter.add(1, {
"infisical.organization.id": organizationId,
"infisical.user.email": email,
"infisical.user.id": userEnc.userId,
"infisical.auth.method": AuthAttemptAuthMethod.EMAIL,
"infisical.auth.result": AuthAttemptAuthResult.SUCCESS,
"client.address": ip,
"user_agent.original": userAgent
});
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
authAttemptCounter.add(1, {
"infisical.organization.id": organizationId,
"infisical.user.email": email,
"infisical.user.id": userEnc.userId,
"infisical.auth.method": AuthAttemptAuthMethod.EMAIL,
"infisical.auth.result": AuthAttemptAuthResult.SUCCESS,
"client.address": ip,
"user_agent.original": userAgent
});
}
return {
tokens: {
@@ -458,13 +462,15 @@ export const authLoginServiceFactory = ({
user: userEnc
} as const;
} catch (error) {
authAttemptCounter.add(1, {
"infisical.user.email": email,
"infisical.auth.method": AuthAttemptAuthMethod.EMAIL,
"infisical.auth.result": AuthAttemptAuthResult.FAILURE,
"client.address": ip,
"user_agent.original": userAgent
});
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
authAttemptCounter.add(1, {
"infisical.user.email": email,
"infisical.auth.method": AuthAttemptAuthMethod.EMAIL,
"infisical.auth.result": AuthAttemptAuthResult.FAILURE,
"client.address": ip,
"user_agent.original": userAgent
});
}
throw error;
}

View File

@@ -1,5 +1,6 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { ForbiddenError } from "@casl/ability";
import { requestContext } from "@fastify/request-context";
import { AxiosError } from "axios";
import { AccessScope, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas";
@@ -22,6 +23,7 @@ import {
} from "@app/lib/errors";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
import { logger } from "@app/lib/logger";
import { AuthAttemptAuthMethod, AuthAttemptAuthResult, authAttemptCounter } from "@app/lib/telemetry/metrics";
import { ActorType, AuthTokenType } from "../auth/auth-type";
import { TIdentityDALFactory } from "../identity/identity-dal";
@@ -65,6 +67,7 @@ export const identityAliCloudAuthServiceFactory = ({
orgDAL
}: TIdentityAliCloudAuthServiceFactoryDep) => {
const login = async ({ identityId, ...params }: TLoginAliCloudAuthDTO) => {
const appCfg = getConfig();
const identityAliCloudAuth = await identityAliCloudAuthDAL.findOne({ identityId });
if (!identityAliCloudAuth) {
throw new NotFoundError({
@@ -75,73 +78,103 @@ export const identityAliCloudAuthServiceFactory = ({
const identity = await identityDAL.findById(identityAliCloudAuth.identityId);
if (!identity) throw new UnauthorizedError({ message: "Identity not found" });
const requestUrl = new URL("https://sts.aliyuncs.com");
const org = await orgDAL.findById(identity.orgId);
for (const key of Object.keys(params)) {
requestUrl.searchParams.set(key, (params as Record<string, string>)[key]);
}
try {
const requestUrl = new URL("https://sts.aliyuncs.com");
const { data } = await request.get<TAliCloudGetUserResponse>(requestUrl.toString()).catch((err: AxiosError) => {
logger.error(err.response, "AliCloudIdentityLogin: Failed to authenticate with Alibaba Cloud");
throw err;
});
for (const key of Object.keys(params)) {
requestUrl.searchParams.set(key, (params as Record<string, string>)[key]);
}
if (identityAliCloudAuth.allowedArns) {
// In the future we could do partial checks for role ARNs
const isAccountAllowed = identityAliCloudAuth.allowedArns.split(",").some((arn) => arn.trim() === data.Arn);
const { data } = await request.get<TAliCloudGetUserResponse>(requestUrl.toString()).catch((err: AxiosError) => {
logger.error(err.response, "AliCloudIdentityLogin: Failed to authenticate with Alibaba Cloud");
throw err;
});
if (!isAccountAllowed)
throw new UnauthorizedError({
message: "Access denied: Alibaba Cloud account ARN not allowed."
});
}
if (identityAliCloudAuth.allowedArns) {
// In the future we could do partial checks for role ARNs
const isAccountAllowed = identityAliCloudAuth.allowedArns.split(",").some((arn) => arn.trim() === data.Arn);
// Generate the token
const identityAccessToken = await identityAliCloudAuthDAL.transaction(async (tx) => {
await membershipIdentityDAL.update(
{ scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id },
{
lastLoginAuthMethod: IdentityAuthMethod.ALICLOUD_AUTH,
lastLoginTime: new Date()
},
tx
);
const newToken = await identityAccessTokenDAL.create(
if (!isAccountAllowed)
throw new UnauthorizedError({
message: "Access denied: Alibaba Cloud account ARN not allowed."
});
}
// Generate the token
const identityAccessToken = await identityAliCloudAuthDAL.transaction(async (tx) => {
await membershipIdentityDAL.update(
{ scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id },
{
lastLoginAuthMethod: IdentityAuthMethod.ALICLOUD_AUTH,
lastLoginTime: new Date()
},
tx
);
const newToken = await identityAccessTokenDAL.create(
{
identityId: identityAliCloudAuth.identityId,
isAccessTokenRevoked: false,
accessTokenTTL: identityAliCloudAuth.accessTokenTTL,
accessTokenMaxTTL: identityAliCloudAuth.accessTokenMaxTTL,
accessTokenNumUses: 0,
accessTokenNumUsesLimit: identityAliCloudAuth.accessTokenNumUsesLimit,
authMethod: IdentityAuthMethod.ALICLOUD_AUTH
},
tx
);
return newToken;
});
const accessToken = crypto.jwt().sign(
{
identityId: identityAliCloudAuth.identityId,
isAccessTokenRevoked: false,
accessTokenTTL: identityAliCloudAuth.accessTokenTTL,
accessTokenMaxTTL: identityAliCloudAuth.accessTokenMaxTTL,
accessTokenNumUses: 0,
accessTokenNumUsesLimit: identityAliCloudAuth.accessTokenNumUsesLimit,
authMethod: IdentityAuthMethod.ALICLOUD_AUTH
},
tx
identityAccessTokenId: identityAccessToken.id,
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
} as TIdentityAccessTokenJwtPayload,
appCfg.AUTH_SECRET,
Number(identityAccessToken.accessTokenTTL) === 0
? undefined
: {
expiresIn: Number(identityAccessToken.accessTokenTTL)
}
);
return newToken;
});
const appCfg = getConfig();
const accessToken = crypto.jwt().sign(
{
identityId: identityAliCloudAuth.identityId,
identityAccessTokenId: identityAccessToken.id,
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
} as TIdentityAccessTokenJwtPayload,
appCfg.AUTH_SECRET,
Number(identityAccessToken.accessTokenTTL) === 0
? undefined
: {
expiresIn: Number(identityAccessToken.accessTokenTTL)
}
);
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
authAttemptCounter.add(1, {
"infisical.identity.id": identityAliCloudAuth.identityId,
"infisical.identity.name": identity.name,
"infisical.organization.id": org.id,
"infisical.organization.name": org.name,
"infisical.identity.auth_method": AuthAttemptAuthMethod.ALICLOUD_AUTH,
"infisical.identity.auth_result": AuthAttemptAuthResult.SUCCESS,
"client.address": requestContext.get("ip"),
"user_agent.original": requestContext.get("userAgent")
});
}
return {
identityAliCloudAuth,
accessToken,
identityAccessToken,
identity
};
return {
identityAliCloudAuth,
accessToken,
identityAccessToken,
identity
};
} catch (error) {
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
authAttemptCounter.add(1, {
"infisical.identity.id": identityAliCloudAuth.identityId,
"infisical.identity.name": identity.name,
"infisical.organization.id": org.id,
"infisical.organization.name": org.name,
"infisical.identity.auth_method": AuthAttemptAuthMethod.ALICLOUD_AUTH,
"infisical.identity.auth_result": AuthAttemptAuthResult.FAILURE,
"client.address": requestContext.get("ip"),
"user_agent.original": requestContext.get("userAgent")
});
}
throw error;
}
};
const attachAliCloudAuth = async ({

View File

@@ -1,5 +1,6 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access */
import { ForbiddenError } from "@casl/ability";
import { requestContext } from "@fastify/request-context";
import axios from "axios";
import RE2 from "re2";
@@ -22,6 +23,7 @@ import {
} from "@app/lib/errors";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
import { logger } from "@app/lib/logger";
import { AuthAttemptAuthMethod, AuthAttemptAuthResult, authAttemptCounter } from "@app/lib/telemetry/metrics";
import { ActorType, AuthTokenType } from "../auth/auth-type";
import { TIdentityDALFactory } from "../identity/identity-dal";
@@ -98,6 +100,7 @@ export const identityAwsAuthServiceFactory = ({
orgDAL
}: TIdentityAwsAuthServiceFactoryDep) => {
const login = async ({ identityId, iamHttpRequestMethod, iamRequestBody, iamRequestHeaders }: TLoginAwsAuthDTO) => {
const appCfg = getConfig();
const identityAwsAuth = await identityAwsAuthDAL.findOne({ identityId });
if (!identityAwsAuth) {
throw new NotFoundError({ message: "AWS auth method not found for identity, did you configure AWS auth?" });
@@ -106,127 +109,156 @@ export const identityAwsAuthServiceFactory = ({
const identity = await identityDAL.findById(identityAwsAuth.identityId);
if (!identity) throw new UnauthorizedError({ message: "Identity not found" });
const headers: TAwsGetCallerIdentityHeaders = JSON.parse(Buffer.from(iamRequestHeaders, "base64").toString());
const body: string = Buffer.from(iamRequestBody, "base64").toString();
const org = await orgDAL.findById(identity.orgId);
try {
const headers: TAwsGetCallerIdentityHeaders = JSON.parse(Buffer.from(iamRequestHeaders, "base64").toString());
const body: string = Buffer.from(iamRequestBody, "base64").toString();
const authHeader = headers.Authorization || headers.authorization;
const region = authHeader ? awsRegionFromHeader(authHeader) : null;
const authHeader = headers.Authorization || headers.authorization;
const region = authHeader ? awsRegionFromHeader(authHeader) : null;
if (!isValidAwsRegion(region)) {
throw new BadRequestError({ message: "Invalid AWS region" });
}
if (!isValidAwsRegion(region)) {
throw new BadRequestError({ message: "Invalid AWS region" });
}
const url = region ? `https://sts.${region}.amazonaws.com` : identityAwsAuth.stsEndpoint;
const url = region ? `https://sts.${region}.amazonaws.com` : identityAwsAuth.stsEndpoint;
const {
data: {
GetCallerIdentityResponse: {
GetCallerIdentityResult: { Account, Arn, UserId }
const {
data: {
GetCallerIdentityResponse: {
GetCallerIdentityResult: { Account, Arn, UserId }
}
}
}: { data: TGetCallerIdentityResponse } = await axios({
method: iamHttpRequestMethod,
url,
headers,
data: body
});
if (identityAwsAuth.allowedAccountIds) {
// validate if Account is in the list of allowed Account IDs
const isAccountAllowed = identityAwsAuth.allowedAccountIds
.split(",")
.map((accountId) => accountId.trim())
.some((accountId) => accountId === Account);
if (!isAccountAllowed)
throw new UnauthorizedError({
message: "Access denied: AWS account ID not allowed."
});
}
if (identityAwsAuth.allowedPrincipalArns) {
// validate if Arn is in the list of allowed Principal ARNs
const formattedArn = extractPrincipalArn(Arn);
const isArnAllowed = identityAwsAuth.allowedPrincipalArns
.split(",")
.map((principalArn) => principalArn.trim())
.some((principalArn) => {
// convert wildcard ARN to a regular expression: "arn:aws:iam::123456789012:*" -> "^arn:aws:iam::123456789012:.*$"
// considers exact matches + wildcard matches
// heavily validated in router
const regex = new RE2(`^${principalArn.replaceAll("*", ".*")}$`);
return regex.test(formattedArn) || regex.test(extractPrincipalArn(Arn, true));
});
if (!isArnAllowed) {
logger.error(
`AWS Auth Login: AWS principal ARN not allowed [principal-arn=${formattedArn}] [raw-arn=${Arn}] [identity-id=${identity.id}]`
);
throw new UnauthorizedError({
message: `Access denied: AWS principal ARN not allowed. [principal-arn=${formattedArn}]`
});
}
}
}: { data: TGetCallerIdentityResponse } = await axios({
method: iamHttpRequestMethod,
url,
headers,
data: body
});
if (identityAwsAuth.allowedAccountIds) {
// validate if Account is in the list of allowed Account IDs
const isAccountAllowed = identityAwsAuth.allowedAccountIds
.split(",")
.map((accountId) => accountId.trim())
.some((accountId) => accountId === Account);
if (!isAccountAllowed)
throw new UnauthorizedError({
message: "Access denied: AWS account ID not allowed."
});
}
if (identityAwsAuth.allowedPrincipalArns) {
// validate if Arn is in the list of allowed Principal ARNs
const formattedArn = extractPrincipalArn(Arn);
const isArnAllowed = identityAwsAuth.allowedPrincipalArns
.split(",")
.map((principalArn) => principalArn.trim())
.some((principalArn) => {
// convert wildcard ARN to a regular expression: "arn:aws:iam::123456789012:*" -> "^arn:aws:iam::123456789012:.*$"
// considers exact matches + wildcard matches
// heavily validated in router
const regex = new RE2(`^${principalArn.replaceAll("*", ".*")}$`);
return regex.test(formattedArn) || regex.test(extractPrincipalArn(Arn, true));
});
if (!isArnAllowed) {
logger.error(
`AWS Auth Login: AWS principal ARN not allowed [principal-arn=${formattedArn}] [raw-arn=${Arn}] [identity-id=${identity.id}]`
const identityAccessToken = await identityAwsAuthDAL.transaction(async (tx) => {
await membershipIdentityDAL.update(
{ scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id },
{
lastLoginAuthMethod: IdentityAuthMethod.AWS_AUTH,
lastLoginTime: new Date()
},
tx
);
const newToken = await identityAccessTokenDAL.create(
{
identityId: identityAwsAuth.identityId,
isAccessTokenRevoked: false,
accessTokenTTL: identityAwsAuth.accessTokenTTL,
accessTokenMaxTTL: identityAwsAuth.accessTokenMaxTTL,
accessTokenNumUses: 0,
accessTokenNumUsesLimit: identityAwsAuth.accessTokenNumUsesLimit,
authMethod: IdentityAuthMethod.AWS_AUTH
},
tx
);
return newToken;
});
throw new UnauthorizedError({
message: `Access denied: AWS principal ARN not allowed. [principal-arn=${formattedArn}]`
});
}
}
const identityAccessToken = await identityAwsAuthDAL.transaction(async (tx) => {
await membershipIdentityDAL.update(
{ scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id },
{
lastLoginAuthMethod: IdentityAuthMethod.AWS_AUTH,
lastLoginTime: new Date()
},
tx
);
const newToken = await identityAccessTokenDAL.create(
const splitArn = extractPrincipalArnEntity(Arn);
const accessToken = crypto.jwt().sign(
{
identityId: identityAwsAuth.identityId,
isAccessTokenRevoked: false,
accessTokenTTL: identityAwsAuth.accessTokenTTL,
accessTokenMaxTTL: identityAwsAuth.accessTokenMaxTTL,
accessTokenNumUses: 0,
accessTokenNumUsesLimit: identityAwsAuth.accessTokenNumUsesLimit,
authMethod: IdentityAuthMethod.AWS_AUTH
},
tx
identityAccessTokenId: identityAccessToken.id,
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN,
identityAuth: {
aws: {
accountId: Account,
arn: Arn,
userId: UserId,
// Derived from ARN
partition: splitArn.Partition,
service: splitArn.Service,
resourceType: splitArn.Type,
resourceName: splitArn.FriendlyName
}
}
} as TIdentityAccessTokenJwtPayload,
appCfg.AUTH_SECRET,
// akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error
Number(identityAccessToken.accessTokenTTL) === 0
? undefined
: {
expiresIn: Number(identityAccessToken.accessTokenTTL)
}
);
return newToken;
});
const appCfg = getConfig();
const splitArn = extractPrincipalArnEntity(Arn);
const accessToken = crypto.jwt().sign(
{
identityId: identityAwsAuth.identityId,
identityAccessTokenId: identityAccessToken.id,
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN,
identityAuth: {
aws: {
accountId: Account,
arn: Arn,
userId: UserId,
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
authAttemptCounter.add(1, {
"infisical.identity.id": identityAwsAuth.identityId,
"infisical.identity.name": identity.name,
"infisical.organization.id": org.id,
"infisical.organization.name": org.name,
"infisical.identity.auth_method": AuthAttemptAuthMethod.AWS_AUTH,
"infisical.identity.auth_result": AuthAttemptAuthResult.SUCCESS,
"client.address": requestContext.get("ip"),
"user_agent.original": requestContext.get("userAgent")
});
}
// Derived from ARN
partition: splitArn.Partition,
service: splitArn.Service,
resourceType: splitArn.Type,
resourceName: splitArn.FriendlyName
}
}
} as TIdentityAccessTokenJwtPayload,
appCfg.AUTH_SECRET,
// akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error
Number(identityAccessToken.accessTokenTTL) === 0
? undefined
: {
expiresIn: Number(identityAccessToken.accessTokenTTL)
}
);
return { accessToken, identityAwsAuth, identityAccessToken, identity };
return { accessToken, identityAwsAuth, identityAccessToken, identity };
} catch (error) {
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
authAttemptCounter.add(1, {
"infisical.identity.id": identityAwsAuth.identityId,
"infisical.identity.name": identity.name,
"infisical.organization.id": org.id,
"infisical.organization.name": org.name,
"infisical.identity.auth_method": AuthAttemptAuthMethod.AWS_AUTH,
"infisical.identity.auth_result": AuthAttemptAuthResult.FAILURE,
"client.address": requestContext.get("ip"),
"user_agent.original": requestContext.get("userAgent")
});
}
throw error;
}
};
const attachAwsAuth = async ({

View File

@@ -1,4 +1,5 @@
import { ForbiddenError } from "@casl/ability";
import { requestContext } from "@fastify/request-context";
import { AccessScope, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
@@ -18,6 +19,7 @@ import {
UnauthorizedError
} from "@app/lib/errors";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
import { AuthAttemptAuthMethod, AuthAttemptAuthResult, authAttemptCounter } from "@app/lib/telemetry/metrics";
import { ActorType, AuthTokenType } from "../auth/auth-type";
import { TIdentityDALFactory } from "../identity/identity-dal";
@@ -61,6 +63,7 @@ export const identityAzureAuthServiceFactory = ({
orgDAL
}: TIdentityAzureAuthServiceFactoryDep) => {
const login = async ({ identityId, jwt: azureJwt }: TLoginAzureAuthDTO) => {
const appCfg = getConfig();
const identityAzureAuth = await identityAzureAuthDAL.findOne({ identityId });
if (!identityAzureAuth) {
throw new NotFoundError({ message: "Azure auth method not found for identity, did you configure Azure Auth?" });
@@ -69,69 +72,99 @@ export const identityAzureAuthServiceFactory = ({
const identity = await identityDAL.findById(identityAzureAuth.identityId);
if (!identity) throw new UnauthorizedError({ message: "Identity not found" });
const azureIdentity = await validateAzureIdentity({
tenantId: identityAzureAuth.tenantId,
resource: identityAzureAuth.resource,
jwt: azureJwt
});
const org = await orgDAL.findById(identity.orgId);
if (azureIdentity.tid !== identityAzureAuth.tenantId)
throw new UnauthorizedError({ message: "Tenant ID mismatch" });
try {
const azureIdentity = await validateAzureIdentity({
tenantId: identityAzureAuth.tenantId,
resource: identityAzureAuth.resource,
jwt: azureJwt
});
if (identityAzureAuth.allowedServicePrincipalIds) {
// validate if the service principal id is in the list of allowed service principal ids
if (azureIdentity.tid !== identityAzureAuth.tenantId)
throw new UnauthorizedError({ message: "Tenant ID mismatch" });
const isServicePrincipalAllowed = identityAzureAuth.allowedServicePrincipalIds
.split(",")
.map((servicePrincipalId) => servicePrincipalId.trim())
.some((servicePrincipalId) => servicePrincipalId === azureIdentity.oid);
if (identityAzureAuth.allowedServicePrincipalIds) {
// validate if the service principal id is in the list of allowed service principal ids
if (!isServicePrincipalAllowed) {
throw new UnauthorizedError({ message: `Service principal '${azureIdentity.oid}' not allowed` });
const isServicePrincipalAllowed = identityAzureAuth.allowedServicePrincipalIds
.split(",")
.map((servicePrincipalId) => servicePrincipalId.trim())
.some((servicePrincipalId) => servicePrincipalId === azureIdentity.oid);
if (!isServicePrincipalAllowed) {
throw new UnauthorizedError({ message: `Service principal '${azureIdentity.oid}' not allowed` });
}
}
}
const identityAccessToken = await identityAzureAuthDAL.transaction(async (tx) => {
await membershipIdentityDAL.update(
{ scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id },
{
lastLoginAuthMethod: IdentityAuthMethod.AZURE_AUTH,
lastLoginTime: new Date()
},
tx
);
const newToken = await identityAccessTokenDAL.create(
const identityAccessToken = await identityAzureAuthDAL.transaction(async (tx) => {
await membershipIdentityDAL.update(
{ scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id },
{
lastLoginAuthMethod: IdentityAuthMethod.AZURE_AUTH,
lastLoginTime: new Date()
},
tx
);
const newToken = await identityAccessTokenDAL.create(
{
identityId: identityAzureAuth.identityId,
isAccessTokenRevoked: false,
accessTokenTTL: identityAzureAuth.accessTokenTTL,
accessTokenMaxTTL: identityAzureAuth.accessTokenMaxTTL,
accessTokenNumUses: 0,
accessTokenNumUsesLimit: identityAzureAuth.accessTokenNumUsesLimit,
authMethod: IdentityAuthMethod.AZURE_AUTH
},
tx
);
return newToken;
});
const accessToken = crypto.jwt().sign(
{
identityId: identityAzureAuth.identityId,
isAccessTokenRevoked: false,
accessTokenTTL: identityAzureAuth.accessTokenTTL,
accessTokenMaxTTL: identityAzureAuth.accessTokenMaxTTL,
accessTokenNumUses: 0,
accessTokenNumUsesLimit: identityAzureAuth.accessTokenNumUsesLimit,
authMethod: IdentityAuthMethod.AZURE_AUTH
},
tx
identityAccessTokenId: identityAccessToken.id,
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
} as TIdentityAccessTokenJwtPayload,
appCfg.AUTH_SECRET,
// akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error
Number(identityAccessToken.accessTokenTTL) === 0
? undefined
: {
expiresIn: Number(identityAccessToken.accessTokenTTL)
}
);
return newToken;
});
const appCfg = getConfig();
const accessToken = crypto.jwt().sign(
{
identityId: identityAzureAuth.identityId,
identityAccessTokenId: identityAccessToken.id,
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
} as TIdentityAccessTokenJwtPayload,
appCfg.AUTH_SECRET,
// akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error
Number(identityAccessToken.accessTokenTTL) === 0
? undefined
: {
expiresIn: Number(identityAccessToken.accessTokenTTL)
}
);
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
authAttemptCounter.add(1, {
"infisical.identity.id": identityAzureAuth.identityId,
"infisical.identity.name": identity.name,
"infisical.organization.id": org.id,
"infisical.organization.name": org.name,
"infisical.identity.auth_method": AuthAttemptAuthMethod.AZURE_AUTH,
"infisical.identity.auth_result": AuthAttemptAuthResult.SUCCESS,
"client.address": requestContext.get("ip"),
"user_agent.original": requestContext.get("userAgent")
});
}
return { accessToken, identityAzureAuth, identityAccessToken, identity };
return { accessToken, identityAzureAuth, identityAccessToken, identity };
} catch (error) {
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
authAttemptCounter.add(1, {
"infisical.identity.id": identityAzureAuth.identityId,
"infisical.identity.name": identity.name,
"infisical.organization.id": org.id,
"infisical.organization.name": org.name,
"infisical.identity.auth_method": AuthAttemptAuthMethod.AZURE_AUTH,
"infisical.identity.auth_result": AuthAttemptAuthResult.FAILURE,
"client.address": requestContext.get("ip"),
"user_agent.original": requestContext.get("userAgent")
});
}
throw error;
}
};
const attachAzureAuth = async ({

View File

@@ -1,4 +1,5 @@
import { ForbiddenError } from "@casl/ability";
import { requestContext } from "@fastify/request-context";
import { AccessScope, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
@@ -18,6 +19,7 @@ import {
UnauthorizedError
} from "@app/lib/errors";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
import { AuthAttemptAuthMethod, AuthAttemptAuthResult, authAttemptCounter } from "@app/lib/telemetry/metrics";
import { ActorType, AuthTokenType } from "../auth/auth-type";
import { TIdentityDALFactory } from "../identity/identity-dal";
@@ -59,6 +61,7 @@ export const identityGcpAuthServiceFactory = ({
orgDAL
}: TIdentityGcpAuthServiceFactoryDep) => {
const login = async ({ identityId, jwt: gcpJwt }: TLoginGcpAuthDTO) => {
const appCfg = getConfig();
const identityGcpAuth = await identityGcpAuthDAL.findOne({ identityId });
if (!identityGcpAuth) {
throw new NotFoundError({ message: "GCP auth method not found for identity, did you configure GCP auth?" });
@@ -67,108 +70,140 @@ export const identityGcpAuthServiceFactory = ({
const identity = await identityDAL.findById(identityGcpAuth.identityId);
if (!identity) throw new UnauthorizedError({ message: "Identity not found" });
let gcpIdentityDetails: TGcpIdentityDetails;
switch (identityGcpAuth.type) {
case "gce": {
gcpIdentityDetails = await validateIdTokenIdentity({
identityId,
jwt: gcpJwt
});
break;
const org = await orgDAL.findById(identity.orgId);
try {
let gcpIdentityDetails: TGcpIdentityDetails;
switch (identityGcpAuth.type) {
case "gce": {
gcpIdentityDetails = await validateIdTokenIdentity({
identityId,
jwt: gcpJwt
});
break;
}
case "iam": {
gcpIdentityDetails = await validateIamIdentity({
identityId,
jwt: gcpJwt
});
break;
}
default: {
throw new BadRequestError({ message: "Invalid GCP Auth type" });
}
}
case "iam": {
gcpIdentityDetails = await validateIamIdentity({
identityId,
jwt: gcpJwt
});
break;
if (identityGcpAuth.allowedServiceAccounts) {
// validate if the service account is in the list of allowed service accounts
const isServiceAccountAllowed = identityGcpAuth.allowedServiceAccounts
.split(",")
.map((serviceAccount) => serviceAccount.trim())
.some((serviceAccount) => serviceAccount === gcpIdentityDetails.email);
if (!isServiceAccountAllowed)
throw new UnauthorizedError({
message: "Access denied: GCP service account not allowed."
});
}
default: {
throw new BadRequestError({ message: "Invalid GCP Auth type" });
if (
identityGcpAuth.type === "gce" &&
identityGcpAuth.allowedProjects &&
gcpIdentityDetails.computeEngineDetails
) {
// validate if the project that the service account belongs to is in the list of allowed projects
const isProjectAllowed = identityGcpAuth.allowedProjects
.split(",")
.map((project) => project.trim())
.some((project) => project === gcpIdentityDetails.computeEngineDetails?.project_id);
if (!isProjectAllowed)
throw new UnauthorizedError({
message: "Access denied: GCP project not allowed."
});
}
}
if (identityGcpAuth.allowedServiceAccounts) {
// validate if the service account is in the list of allowed service accounts
if (identityGcpAuth.type === "gce" && identityGcpAuth.allowedZones && gcpIdentityDetails.computeEngineDetails) {
const isZoneAllowed = identityGcpAuth.allowedZones
.split(",")
.map((zone) => zone.trim())
.some((zone) => zone === gcpIdentityDetails.computeEngineDetails?.zone);
const isServiceAccountAllowed = identityGcpAuth.allowedServiceAccounts
.split(",")
.map((serviceAccount) => serviceAccount.trim())
.some((serviceAccount) => serviceAccount === gcpIdentityDetails.email);
if (!isZoneAllowed)
throw new UnauthorizedError({
message: "Access denied: GCP zone not allowed."
});
}
if (!isServiceAccountAllowed)
throw new UnauthorizedError({
message: "Access denied: GCP service account not allowed."
});
}
if (identityGcpAuth.type === "gce" && identityGcpAuth.allowedProjects && gcpIdentityDetails.computeEngineDetails) {
// validate if the project that the service account belongs to is in the list of allowed projects
const isProjectAllowed = identityGcpAuth.allowedProjects
.split(",")
.map((project) => project.trim())
.some((project) => project === gcpIdentityDetails.computeEngineDetails?.project_id);
if (!isProjectAllowed)
throw new UnauthorizedError({
message: "Access denied: GCP project not allowed."
});
}
if (identityGcpAuth.type === "gce" && identityGcpAuth.allowedZones && gcpIdentityDetails.computeEngineDetails) {
const isZoneAllowed = identityGcpAuth.allowedZones
.split(",")
.map((zone) => zone.trim())
.some((zone) => zone === gcpIdentityDetails.computeEngineDetails?.zone);
if (!isZoneAllowed)
throw new UnauthorizedError({
message: "Access denied: GCP zone not allowed."
});
}
const identityAccessToken = await identityGcpAuthDAL.transaction(async (tx) => {
await membershipIdentityDAL.update(
{ scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id },
{
lastLoginAuthMethod: IdentityAuthMethod.GCP_AUTH,
lastLoginTime: new Date()
},
tx
);
const newToken = await identityAccessTokenDAL.create(
const identityAccessToken = await identityGcpAuthDAL.transaction(async (tx) => {
await membershipIdentityDAL.update(
{ scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id },
{
lastLoginAuthMethod: IdentityAuthMethod.GCP_AUTH,
lastLoginTime: new Date()
},
tx
);
const newToken = await identityAccessTokenDAL.create(
{
identityId: identityGcpAuth.identityId,
isAccessTokenRevoked: false,
accessTokenTTL: identityGcpAuth.accessTokenTTL,
accessTokenMaxTTL: identityGcpAuth.accessTokenMaxTTL,
accessTokenNumUses: 0,
accessTokenNumUsesLimit: identityGcpAuth.accessTokenNumUsesLimit,
authMethod: IdentityAuthMethod.GCP_AUTH
},
tx
);
return newToken;
});
const accessToken = crypto.jwt().sign(
{
identityId: identityGcpAuth.identityId,
isAccessTokenRevoked: false,
accessTokenTTL: identityGcpAuth.accessTokenTTL,
accessTokenMaxTTL: identityGcpAuth.accessTokenMaxTTL,
accessTokenNumUses: 0,
accessTokenNumUsesLimit: identityGcpAuth.accessTokenNumUsesLimit,
authMethod: IdentityAuthMethod.GCP_AUTH
},
tx
identityAccessTokenId: identityAccessToken.id,
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
} as TIdentityAccessTokenJwtPayload,
appCfg.AUTH_SECRET,
// akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error
Number(identityAccessToken.accessTokenTTL) === 0
? undefined
: {
expiresIn: Number(identityAccessToken.accessTokenTTL)
}
);
return newToken;
});
const appCfg = getConfig();
const accessToken = crypto.jwt().sign(
{
identityId: identityGcpAuth.identityId,
identityAccessTokenId: identityAccessToken.id,
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
} as TIdentityAccessTokenJwtPayload,
appCfg.AUTH_SECRET,
// akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error
Number(identityAccessToken.accessTokenTTL) === 0
? undefined
: {
expiresIn: Number(identityAccessToken.accessTokenTTL)
}
);
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
authAttemptCounter.add(1, {
"infisical.identity.id": identityGcpAuth.identityId,
"infisical.identity.name": identity.name,
"infisical.organization.id": org.id,
"infisical.organization.name": org.name,
"infisical.identity.auth_method": AuthAttemptAuthMethod.GCP_AUTH,
"infisical.identity.auth_result": AuthAttemptAuthResult.SUCCESS,
"client.address": requestContext.get("ip"),
"user_agent.original": requestContext.get("userAgent")
});
}
return { accessToken, identityGcpAuth, identityAccessToken, identity };
return { accessToken, identityGcpAuth, identityAccessToken, identity };
} catch (error) {
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
authAttemptCounter.add(1, {
"infisical.identity.id": identityGcpAuth.identityId,
"infisical.identity.name": identity.name,
"infisical.organization.id": org.id,
"infisical.organization.name": org.name,
"infisical.identity.auth_method": AuthAttemptAuthMethod.GCP_AUTH,
"infisical.identity.auth_result": AuthAttemptAuthResult.FAILURE,
"client.address": requestContext.get("ip"),
"user_agent.original": requestContext.get("userAgent")
});
}
throw error;
}
};
const attachGcpAuth = async ({

View File

@@ -1,4 +1,5 @@
import { ForbiddenError } from "@casl/ability";
import { requestContext } from "@fastify/request-context";
import https from "https";
import jwt from "jsonwebtoken";
import { JwksClient } from "jwks-rsa";
@@ -21,6 +22,7 @@ import {
UnauthorizedError
} from "@app/lib/errors";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
import { AuthAttemptAuthMethod, AuthAttemptAuthResult, authAttemptCounter } from "@app/lib/telemetry/metrics";
import { getValueByDot } from "@app/lib/template/dot-access";
import { ActorType, AuthTokenType } from "../auth/auth-type";
@@ -67,6 +69,7 @@ export const identityJwtAuthServiceFactory = ({
orgDAL
}: TIdentityJwtAuthServiceFactoryDep) => {
const login = async ({ identityId, jwt: jwtValue }: TLoginJwtAuthDTO) => {
const appCfg = getConfig();
const identityJwtAuth = await identityJwtAuthDAL.findOne({ identityId });
if (!identityJwtAuth) {
throw new NotFoundError({ message: "JWT auth method not found for identity, did you configure JWT auth?" });
@@ -75,176 +78,205 @@ export const identityJwtAuthServiceFactory = ({
const identity = await identityDAL.findById(identityJwtAuth.identityId);
if (!identity) throw new UnauthorizedError({ message: "Identity not found" });
const { decryptor: orgDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization,
orgId: identity.orgId
});
const decodedToken = crypto.jwt().decode(jwtValue, { complete: true });
if (!decodedToken) {
throw new UnauthorizedError({
message: "Invalid JWT"
const org = await orgDAL.findById(identity.orgId);
try {
const { decryptor: orgDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization,
orgId: identity.orgId
});
}
let tokenData: Record<string, string | boolean | number> = {};
if (identityJwtAuth.configurationType === JwtConfigurationType.JWKS) {
let client: JwksClient;
if (identityJwtAuth.jwksUrl.includes("https:")) {
const decryptedJwksCaCert = orgDataKeyDecryptor({
cipherTextBlob: identityJwtAuth.encryptedJwksCaCert
}).toString();
const requestAgent = new https.Agent({ ca: decryptedJwksCaCert, rejectUnauthorized: !!decryptedJwksCaCert });
client = new JwksClient({
jwksUri: identityJwtAuth.jwksUrl,
requestAgent
});
} else {
client = new JwksClient({
jwksUri: identityJwtAuth.jwksUrl
const decodedToken = crypto.jwt().decode(jwtValue, { complete: true });
if (!decodedToken) {
throw new UnauthorizedError({
message: "Invalid JWT"
});
}
const { kid } = decodedToken.header as { kid: string };
const jwtSigningKey = await client.getSigningKey(kid);
let tokenData: Record<string, string | boolean | number> = {};
try {
tokenData = crypto.jwt().verify(jwtValue, jwtSigningKey.getPublicKey()) as Record<string, string>;
} catch (error) {
if (error instanceof jwt.JsonWebTokenError) {
throw new UnauthorizedError({
message: `Access denied: ${error.message}`
if (identityJwtAuth.configurationType === JwtConfigurationType.JWKS) {
let client: JwksClient;
if (identityJwtAuth.jwksUrl.includes("https:")) {
const decryptedJwksCaCert = orgDataKeyDecryptor({
cipherTextBlob: identityJwtAuth.encryptedJwksCaCert
}).toString();
const requestAgent = new https.Agent({ ca: decryptedJwksCaCert, rejectUnauthorized: !!decryptedJwksCaCert });
client = new JwksClient({
jwksUri: identityJwtAuth.jwksUrl,
requestAgent
});
} else {
client = new JwksClient({
jwksUri: identityJwtAuth.jwksUrl
});
}
throw error;
}
} else {
const decryptedPublicKeys = orgDataKeyDecryptor({ cipherTextBlob: identityJwtAuth.encryptedPublicKeys })
.toString()
.split(",");
const { kid } = decodedToken.header as { kid: string };
const jwtSigningKey = await client.getSigningKey(kid);
const errors: string[] = [];
let isMatchAnyKey = false;
for (const publicKey of decryptedPublicKeys) {
try {
tokenData = crypto.jwt().verify(jwtValue, publicKey) as Record<string, string>;
isMatchAnyKey = true;
tokenData = crypto.jwt().verify(jwtValue, jwtSigningKey.getPublicKey()) as Record<string, string>;
} catch (error) {
if (error instanceof jwt.JsonWebTokenError) {
errors.push(error.message);
throw new UnauthorizedError({
message: `Access denied: ${error.message}`
});
}
throw error;
}
} else {
const decryptedPublicKeys = orgDataKeyDecryptor({ cipherTextBlob: identityJwtAuth.encryptedPublicKeys })
.toString()
.split(",");
const errors: string[] = [];
let isMatchAnyKey = false;
for (const publicKey of decryptedPublicKeys) {
try {
tokenData = crypto.jwt().verify(jwtValue, publicKey) as Record<string, string>;
isMatchAnyKey = true;
} catch (error) {
if (error instanceof jwt.JsonWebTokenError) {
errors.push(error.message);
}
}
}
}
if (!isMatchAnyKey) {
throw new UnauthorizedError({
message: `Access denied: JWT verification failed with all keys. Errors - ${errors.join("; ")}`
});
}
}
if (identityJwtAuth.boundIssuer) {
if (tokenData.iss !== identityJwtAuth.boundIssuer) {
throw new ForbiddenRequestError({
message: "Access denied: issuer mismatch"
});
}
}
if (identityJwtAuth.boundSubject) {
if (!tokenData.sub) {
throw new UnauthorizedError({
message: "Access denied: token has no subject field"
});
}
if (!doesFieldValueMatchJwtPolicy(tokenData.sub, identityJwtAuth.boundSubject)) {
throw new ForbiddenRequestError({
message: "Access denied: subject not allowed"
});
}
}
if (identityJwtAuth.boundAudiences) {
if (!tokenData.aud) {
throw new UnauthorizedError({
message: "Access denied: token has no audience field"
});
}
if (
!identityJwtAuth.boundAudiences
.split(", ")
.some((policyValue) => doesFieldValueMatchJwtPolicy(tokenData.aud, policyValue))
) {
throw new UnauthorizedError({
message: "Access denied: token audience not allowed"
});
}
}
if (identityJwtAuth.boundClaims) {
Object.keys(identityJwtAuth.boundClaims).forEach((claimKey) => {
const claimValue = (identityJwtAuth.boundClaims as Record<string, string>)[claimKey];
const value = getValueByDot(tokenData, claimKey);
if (!value) {
if (!isMatchAnyKey) {
throw new UnauthorizedError({
message: `Access denied: token has no ${claimKey} field`
message: `Access denied: JWT verification failed with all keys. Errors - ${errors.join("; ")}`
});
}
}
if (identityJwtAuth.boundIssuer) {
if (tokenData.iss !== identityJwtAuth.boundIssuer) {
throw new ForbiddenRequestError({
message: "Access denied: issuer mismatch"
});
}
}
if (identityJwtAuth.boundSubject) {
if (!tokenData.sub) {
throw new UnauthorizedError({
message: "Access denied: token has no subject field"
});
}
// handle both single and multi-valued claims
if (!claimValue.split(", ").some((claimEntry) => doesFieldValueMatchJwtPolicy(value, claimEntry))) {
throw new UnauthorizedError({
message: `Access denied: claim mismatch for field ${claimKey}`
if (!doesFieldValueMatchJwtPolicy(tokenData.sub, identityJwtAuth.boundSubject)) {
throw new ForbiddenRequestError({
message: "Access denied: subject not allowed"
});
}
}
if (identityJwtAuth.boundAudiences) {
if (!tokenData.aud) {
throw new UnauthorizedError({
message: "Access denied: token has no audience field"
});
}
if (
!identityJwtAuth.boundAudiences
.split(", ")
.some((policyValue) => doesFieldValueMatchJwtPolicy(tokenData.aud, policyValue))
) {
throw new UnauthorizedError({
message: "Access denied: token audience not allowed"
});
}
}
if (identityJwtAuth.boundClaims) {
Object.keys(identityJwtAuth.boundClaims).forEach((claimKey) => {
const claimValue = (identityJwtAuth.boundClaims as Record<string, string>)[claimKey];
const value = getValueByDot(tokenData, claimKey);
if (!value) {
throw new UnauthorizedError({
message: `Access denied: token has no ${claimKey} field`
});
}
// handle both single and multi-valued claims
if (!claimValue.split(", ").some((claimEntry) => doesFieldValueMatchJwtPolicy(value, claimEntry))) {
throw new UnauthorizedError({
message: `Access denied: claim mismatch for field ${claimKey}`
});
}
});
}
const identityAccessToken = await identityJwtAuthDAL.transaction(async (tx) => {
await membershipIdentityDAL.update(
{ scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id },
{ lastLoginAuthMethod: IdentityAuthMethod.JWT_AUTH, lastLoginTime: new Date() },
tx
);
const newToken = await identityAccessTokenDAL.create(
{
identityId: identityJwtAuth.identityId,
isAccessTokenRevoked: false,
accessTokenTTL: identityJwtAuth.accessTokenTTL,
accessTokenMaxTTL: identityJwtAuth.accessTokenMaxTTL,
accessTokenNumUses: 0,
accessTokenNumUsesLimit: identityJwtAuth.accessTokenNumUsesLimit,
authMethod: IdentityAuthMethod.JWT_AUTH
},
tx
);
return newToken;
});
}
const identityAccessToken = await identityJwtAuthDAL.transaction(async (tx) => {
await membershipIdentityDAL.update(
{ scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id },
{ lastLoginAuthMethod: IdentityAuthMethod.JWT_AUTH, lastLoginTime: new Date() },
tx
);
const newToken = await identityAccessTokenDAL.create(
const accessToken = crypto.jwt().sign(
{
identityId: identityJwtAuth.identityId,
isAccessTokenRevoked: false,
accessTokenTTL: identityJwtAuth.accessTokenTTL,
accessTokenMaxTTL: identityJwtAuth.accessTokenMaxTTL,
accessTokenNumUses: 0,
accessTokenNumUsesLimit: identityJwtAuth.accessTokenNumUsesLimit,
authMethod: IdentityAuthMethod.JWT_AUTH
},
tx
identityAccessTokenId: identityAccessToken.id,
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
} as TIdentityAccessTokenJwtPayload,
appCfg.AUTH_SECRET,
// akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error
Number(identityAccessToken.accessTokenTTL) === 0
? undefined
: {
expiresIn: Number(identityAccessToken.accessTokenTTL)
}
);
return newToken;
});
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
authAttemptCounter.add(1, {
"infisical.identity.id": identityJwtAuth.identityId,
"infisical.identity.name": identity.name,
"infisical.organization.id": org.id,
"infisical.organization.name": org.name,
"infisical.identity.auth_method": AuthAttemptAuthMethod.JWT_AUTH,
"infisical.identity.auth_result": AuthAttemptAuthResult.SUCCESS,
"client.address": requestContext.get("ip"),
"user_agent.original": requestContext.get("userAgent")
});
}
const appCfg = getConfig();
const accessToken = crypto.jwt().sign(
{
identityId: identityJwtAuth.identityId,
identityAccessTokenId: identityAccessToken.id,
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
} as TIdentityAccessTokenJwtPayload,
appCfg.AUTH_SECRET,
// akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error
Number(identityAccessToken.accessTokenTTL) === 0
? undefined
: {
expiresIn: Number(identityAccessToken.accessTokenTTL)
}
);
return { accessToken, identityJwtAuth, identityAccessToken, identity };
return { accessToken, identityJwtAuth, identityAccessToken, identity };
} catch (error) {
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
authAttemptCounter.add(1, {
"infisical.identity.id": identityJwtAuth.identityId,
"infisical.identity.name": identity.name,
"infisical.organization.id": org.id,
"infisical.organization.name": org.name,
"infisical.identity.auth_method": AuthAttemptAuthMethod.JWT_AUTH,
"infisical.identity.auth_result": AuthAttemptAuthResult.FAILURE,
"client.address": requestContext.get("ip"),
"user_agent.original": requestContext.get("userAgent")
});
}
throw error;
}
};
const attachJwtAuth = async ({

View File

@@ -1,4 +1,5 @@
import { ForbiddenError } from "@casl/ability";
import { requestContext } from "@fastify/request-context";
import axios, { AxiosError } from "axios";
import https from "https";
import RE2 from "re2";
@@ -37,6 +38,7 @@ import { GatewayHttpProxyActions, GatewayProxyProtocol, withGatewayProxy } from
import { withGatewayV2Proxy } from "@app/lib/gateway-v2/gateway-v2";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
import { logger } from "@app/lib/logger";
import { AuthAttemptAuthMethod, AuthAttemptAuthResult, authAttemptCounter } from "@app/lib/telemetry/metrics";
import { ActorType, AuthTokenType } from "../auth/auth-type";
import { TIdentityDALFactory } from "../identity/identity-dal";
@@ -182,6 +184,7 @@ export const identityKubernetesAuthServiceFactory = ({
};
const login = async ({ identityId, jwt: serviceAccountJwt }: TLoginKubernetesAuthDTO) => {
const appCfg = getConfig();
const identityKubernetesAuth = await identityKubernetesAuthDAL.findOne({ identityId });
if (!identityKubernetesAuth) {
throw new NotFoundError({
@@ -192,294 +195,328 @@ export const identityKubernetesAuthServiceFactory = ({
const identity = await identityDAL.findById(identityKubernetesAuth.identityId);
if (!identity) throw new UnauthorizedError({ message: "Identity not found" });
const { decryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization,
orgId: identity.orgId
});
const org = await orgDAL.findById(identity.orgId);
let caCert = "";
if (identityKubernetesAuth.encryptedKubernetesCaCertificate) {
caCert = decryptor({ cipherTextBlob: identityKubernetesAuth.encryptedKubernetesCaCertificate }).toString();
}
try {
const { decryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization,
orgId: identity.orgId
});
const tokenReviewCallbackRaw = async (host = identityKubernetesAuth.kubernetesHost, port?: number) => {
logger.info({ host, port }, "tokenReviewCallbackRaw: Processing kubernetes token review using raw API");
if (!host || !identityKubernetesAuth.kubernetesHost) {
throw new BadRequestError({
message: "Kubernetes host is required when token review mode is set to API"
});
let caCert = "";
if (identityKubernetesAuth.encryptedKubernetesCaCertificate) {
caCert = decryptor({ cipherTextBlob: identityKubernetesAuth.encryptedKubernetesCaCertificate }).toString();
}
let tokenReviewerJwt = "";
if (identityKubernetesAuth.encryptedKubernetesTokenReviewerJwt) {
tokenReviewerJwt = decryptor({
cipherTextBlob: identityKubernetesAuth.encryptedKubernetesTokenReviewerJwt
}).toString();
} else {
// if no token reviewer is provided means the incoming token has to act as reviewer
tokenReviewerJwt = serviceAccountJwt;
}
const tokenReviewCallbackRaw = async (host = identityKubernetesAuth.kubernetesHost, port?: number) => {
logger.info({ host, port }, "tokenReviewCallbackRaw: Processing kubernetes token review using raw API");
let servername = identityKubernetesAuth.kubernetesHost;
if (servername.startsWith("https://") || servername.startsWith("http://")) {
servername = new RE2("^https?:\\/\\/").replace(servername, "");
}
if (!host || !identityKubernetesAuth.kubernetesHost) {
throw new BadRequestError({
message: "Kubernetes host is required when token review mode is set to API"
});
}
// get the last colon index, if it has a port, remove it, including the colon
const lastColonIndex = servername.lastIndexOf(":");
if (lastColonIndex !== -1) {
servername = servername.substring(0, lastColonIndex);
}
let tokenReviewerJwt = "";
if (identityKubernetesAuth.encryptedKubernetesTokenReviewerJwt) {
tokenReviewerJwt = decryptor({
cipherTextBlob: identityKubernetesAuth.encryptedKubernetesTokenReviewerJwt
}).toString();
} else {
// if no token reviewer is provided means the incoming token has to act as reviewer
tokenReviewerJwt = serviceAccountJwt;
}
const baseUrl = port ? `${host}:${port}` : host;
let servername = identityKubernetesAuth.kubernetesHost;
if (servername.startsWith("https://") || servername.startsWith("http://")) {
servername = new RE2("^https?:\\/\\/").replace(servername, "");
}
const res = await axios
.post<TCreateTokenReviewResponse>(
`${baseUrl}/apis/authentication.k8s.io/v1/tokenreviews`,
{
apiVersion: "authentication.k8s.io/v1",
kind: "TokenReview",
spec: {
token: serviceAccountJwt,
...(identityKubernetesAuth.allowedAudience ? { audiences: [identityKubernetesAuth.allowedAudience] } : {})
}
},
{
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${tokenReviewerJwt}`
// get the last colon index, if it has a port, remove it, including the colon
const lastColonIndex = servername.lastIndexOf(":");
if (lastColonIndex !== -1) {
servername = servername.substring(0, lastColonIndex);
}
const baseUrl = port ? `${host}:${port}` : host;
const res = await axios
.post<TCreateTokenReviewResponse>(
`${baseUrl}/apis/authentication.k8s.io/v1/tokenreviews`,
{
apiVersion: "authentication.k8s.io/v1",
kind: "TokenReview",
spec: {
token: serviceAccountJwt,
...(identityKubernetesAuth.allowedAudience
? { audiences: [identityKubernetesAuth.allowedAudience] }
: {})
}
},
signal: AbortSignal.timeout(10000),
timeout: 10000,
httpsAgent: new https.Agent({
ca: caCert,
rejectUnauthorized: Boolean(caCert),
servername
})
}
)
.catch((err) => {
if (err instanceof AxiosError) {
if (err.response) {
const { message } = err?.response?.data as unknown as { message?: string };
if (message) {
throw new UnauthorizedError({
message,
name: "KubernetesTokenReviewRequestError"
});
}
}
}
throw err;
});
return res.data;
};
const tokenReviewCallbackThroughGateway = async (host: string, port?: number) => {
logger.info(
{
host,
port
},
"tokenReviewCallbackThroughGateway: Processing kubernetes token review using gateway"
);
const res = await axios
.post<TCreateTokenReviewResponse>(
`${host}:${port}/apis/authentication.k8s.io/v1/tokenreviews`,
{
apiVersion: "authentication.k8s.io/v1",
kind: "TokenReview",
spec: {
token: serviceAccountJwt,
...(identityKubernetesAuth.allowedAudience ? { audiences: [identityKubernetesAuth.allowedAudience] } : {})
}
},
{
headers: {
"Content-Type": "application/json",
"x-infisical-action": GatewayHttpProxyActions.UseGatewayK8sServiceAccount
},
signal: AbortSignal.timeout(10000),
timeout: 10000
}
)
.catch((err) => {
if (err instanceof AxiosError) {
if (err.response) {
let { message } = err?.response?.data as unknown as { message?: string };
if (!message && typeof err.response.data === "string") {
message = err.response.data;
}
if (message) {
throw new UnauthorizedError({
message,
name: "KubernetesTokenReviewRequestError"
});
}
}
}
throw err;
});
return res.data;
};
let data: TCreateTokenReviewResponse | undefined;
if (identityKubernetesAuth.tokenReviewMode === IdentityKubernetesAuthTokenReviewMode.Gateway) {
if (!identityKubernetesAuth.gatewayId && !identityKubernetesAuth.gatewayV2Id) {
throw new BadRequestError({
message: "Gateway ID is required when token review mode is set to Gateway"
});
}
data = await $gatewayProxyWrapper(
{
gatewayId: (identityKubernetesAuth.gatewayV2Id ?? identityKubernetesAuth.gatewayId) as string,
reviewTokenThroughGateway: true
},
tokenReviewCallbackThroughGateway
);
} else if (identityKubernetesAuth.tokenReviewMode === IdentityKubernetesAuthTokenReviewMode.Api) {
if (!identityKubernetesAuth.kubernetesHost) {
throw new BadRequestError({
message: "Kubernetes host is required when token review mode is set to API"
});
}
let { kubernetesHost } = identityKubernetesAuth;
if (kubernetesHost.startsWith("https://") || kubernetesHost.startsWith("http://")) {
kubernetesHost = new RE2("^https?:\\/\\/").replace(kubernetesHost, "");
}
const [k8sHost, k8sPort] = kubernetesHost.split(":");
data =
identityKubernetesAuth.gatewayId || identityKubernetesAuth.gatewayV2Id
? await $gatewayProxyWrapper(
{
gatewayId: (identityKubernetesAuth.gatewayV2Id ?? identityKubernetesAuth.gatewayId) as string,
targetHost: k8sHost,
targetPort: k8sPort ? Number(k8sPort) : 443,
reviewTokenThroughGateway: false
{
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${tokenReviewerJwt}`
},
tokenReviewCallbackRaw
)
: await tokenReviewCallbackRaw();
} else {
throw new BadRequestError({
message: `Invalid token review mode: ${identityKubernetesAuth.tokenReviewMode}`
});
}
signal: AbortSignal.timeout(10000),
timeout: 10000,
httpsAgent: new https.Agent({
ca: caCert,
rejectUnauthorized: Boolean(caCert),
servername
})
}
)
.catch((err) => {
if (err instanceof AxiosError) {
if (err.response) {
const { message } = err?.response?.data as unknown as { message?: string };
if (!data) {
throw new BadRequestError({
message: "Failed to review token"
});
}
if (message) {
throw new UnauthorizedError({
message,
name: "KubernetesTokenReviewRequestError"
});
}
}
}
throw err;
});
if ("error" in data.status)
throw new UnauthorizedError({ message: data.status.error, name: "KubernetesTokenReviewError" });
return res.data;
};
// check the response to determine if the token is valid
if (!(data.status && data.status.authenticated))
throw new UnauthorizedError({
message: "Kubernetes token not authenticated",
name: "KubernetesTokenReviewError"
const tokenReviewCallbackThroughGateway = async (host: string, port?: number) => {
logger.info(
{
host,
port
},
"tokenReviewCallbackThroughGateway: Processing kubernetes token review using gateway"
);
const res = await axios
.post<TCreateTokenReviewResponse>(
`${host}:${port}/apis/authentication.k8s.io/v1/tokenreviews`,
{
apiVersion: "authentication.k8s.io/v1",
kind: "TokenReview",
spec: {
token: serviceAccountJwt,
...(identityKubernetesAuth.allowedAudience
? { audiences: [identityKubernetesAuth.allowedAudience] }
: {})
}
},
{
headers: {
"Content-Type": "application/json",
"x-infisical-action": GatewayHttpProxyActions.UseGatewayK8sServiceAccount
},
signal: AbortSignal.timeout(10000),
timeout: 10000
}
)
.catch((err) => {
if (err instanceof AxiosError) {
if (err.response) {
let { message } = err?.response?.data as unknown as { message?: string };
if (!message && typeof err.response.data === "string") {
message = err.response.data;
}
if (message) {
throw new UnauthorizedError({
message,
name: "KubernetesTokenReviewRequestError"
});
}
}
}
throw err;
});
return res.data;
};
let data: TCreateTokenReviewResponse | undefined;
if (identityKubernetesAuth.tokenReviewMode === IdentityKubernetesAuthTokenReviewMode.Gateway) {
if (!identityKubernetesAuth.gatewayId && !identityKubernetesAuth.gatewayV2Id) {
throw new BadRequestError({
message: "Gateway ID is required when token review mode is set to Gateway"
});
}
data = await $gatewayProxyWrapper(
{
gatewayId: (identityKubernetesAuth.gatewayV2Id ?? identityKubernetesAuth.gatewayId) as string,
reviewTokenThroughGateway: true
},
tokenReviewCallbackThroughGateway
);
} else if (identityKubernetesAuth.tokenReviewMode === IdentityKubernetesAuthTokenReviewMode.Api) {
if (!identityKubernetesAuth.kubernetesHost) {
throw new BadRequestError({
message: "Kubernetes host is required when token review mode is set to API"
});
}
let { kubernetesHost } = identityKubernetesAuth;
if (kubernetesHost.startsWith("https://") || kubernetesHost.startsWith("http://")) {
kubernetesHost = new RE2("^https?:\\/\\/").replace(kubernetesHost, "");
}
const [k8sHost, k8sPort] = kubernetesHost.split(":");
data =
identityKubernetesAuth.gatewayId || identityKubernetesAuth.gatewayV2Id
? await $gatewayProxyWrapper(
{
gatewayId: (identityKubernetesAuth.gatewayV2Id ?? identityKubernetesAuth.gatewayId) as string,
targetHost: k8sHost,
targetPort: k8sPort ? Number(k8sPort) : 443,
reviewTokenThroughGateway: false
},
tokenReviewCallbackRaw
)
: await tokenReviewCallbackRaw();
} else {
throw new BadRequestError({
message: `Invalid token review mode: ${identityKubernetesAuth.tokenReviewMode}`
});
}
if (!data) {
throw new BadRequestError({
message: "Failed to review token"
});
}
if ("error" in data.status)
throw new UnauthorizedError({ message: data.status.error, name: "KubernetesTokenReviewError" });
// check the response to determine if the token is valid
if (!(data.status && data.status.authenticated))
throw new UnauthorizedError({
message: "Kubernetes token not authenticated",
name: "KubernetesTokenReviewError"
});
const { namespace: targetNamespace, name: targetName } = extractK8sUsername(data.status.user.username);
if (identityKubernetesAuth.allowedNamespaces) {
// validate if [targetNamespace] is in the list of allowed namespaces
const isNamespaceAllowed = identityKubernetesAuth.allowedNamespaces
.split(",")
.map((namespace) => namespace.trim())
.some((namespace) => namespace === targetNamespace);
if (!isNamespaceAllowed)
throw new UnauthorizedError({
message: "Access denied: K8s namespace not allowed."
});
}
if (identityKubernetesAuth.allowedNames) {
// validate if [targetName] is in the list of allowed names
const isNameAllowed = identityKubernetesAuth.allowedNames
.split(",")
.map((name) => name.trim())
.some((name) => name === targetName);
if (!isNameAllowed)
throw new UnauthorizedError({
message: "Access denied: K8s name not allowed."
});
}
if (identityKubernetesAuth.allowedAudience) {
// validate if [audience] is in the list of allowed audiences
const isAudienceAllowed = data.status.audiences.some(
(audience) => audience === identityKubernetesAuth.allowedAudience
);
if (!isAudienceAllowed)
throw new UnauthorizedError({
message: "Access denied: K8s audience not allowed."
});
}
const identityAccessToken = await identityKubernetesAuthDAL.transaction(async (tx) => {
await membershipIdentityDAL.update(
{ scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id },
{ lastLoginAuthMethod: IdentityAuthMethod.KUBERNETES_AUTH, lastLoginTime: new Date() },
tx
);
const newToken = await identityAccessTokenDAL.create(
{
identityId: identityKubernetesAuth.identityId,
isAccessTokenRevoked: false,
accessTokenTTL: identityKubernetesAuth.accessTokenTTL,
accessTokenMaxTTL: identityKubernetesAuth.accessTokenMaxTTL,
accessTokenNumUses: 0,
accessTokenNumUsesLimit: identityKubernetesAuth.accessTokenNumUsesLimit,
authMethod: IdentityAuthMethod.KUBERNETES_AUTH
},
tx
);
return newToken;
});
const { namespace: targetNamespace, name: targetName } = extractK8sUsername(data.status.user.username);
if (identityKubernetesAuth.allowedNamespaces) {
// validate if [targetNamespace] is in the list of allowed namespaces
const isNamespaceAllowed = identityKubernetesAuth.allowedNamespaces
.split(",")
.map((namespace) => namespace.trim())
.some((namespace) => namespace === targetNamespace);
if (!isNamespaceAllowed)
throw new UnauthorizedError({
message: "Access denied: K8s namespace not allowed."
});
}
if (identityKubernetesAuth.allowedNames) {
// validate if [targetName] is in the list of allowed names
const isNameAllowed = identityKubernetesAuth.allowedNames
.split(",")
.map((name) => name.trim())
.some((name) => name === targetName);
if (!isNameAllowed)
throw new UnauthorizedError({
message: "Access denied: K8s name not allowed."
});
}
if (identityKubernetesAuth.allowedAudience) {
// validate if [audience] is in the list of allowed audiences
const isAudienceAllowed = data.status.audiences.some(
(audience) => audience === identityKubernetesAuth.allowedAudience
);
if (!isAudienceAllowed)
throw new UnauthorizedError({
message: "Access denied: K8s audience not allowed."
});
}
const identityAccessToken = await identityKubernetesAuthDAL.transaction(async (tx) => {
await membershipIdentityDAL.update(
{ scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id },
{ lastLoginAuthMethod: IdentityAuthMethod.KUBERNETES_AUTH, lastLoginTime: new Date() },
tx
);
const newToken = await identityAccessTokenDAL.create(
const accessToken = crypto.jwt().sign(
{
identityId: identityKubernetesAuth.identityId,
isAccessTokenRevoked: false,
accessTokenTTL: identityKubernetesAuth.accessTokenTTL,
accessTokenMaxTTL: identityKubernetesAuth.accessTokenMaxTTL,
accessTokenNumUses: 0,
accessTokenNumUsesLimit: identityKubernetesAuth.accessTokenNumUsesLimit,
authMethod: IdentityAuthMethod.KUBERNETES_AUTH
},
tx
identityAccessTokenId: identityAccessToken.id,
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN,
identityAuth: {
kubernetes: {
namespace: targetNamespace,
name: targetName
}
}
} as TIdentityAccessTokenJwtPayload,
appCfg.AUTH_SECRET,
// akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error
Number(identityAccessToken.accessTokenTTL) === 0
? undefined
: {
expiresIn: Number(identityAccessToken.accessTokenTTL)
}
);
return newToken;
});
const appCfg = getConfig();
const accessToken = crypto.jwt().sign(
{
identityId: identityKubernetesAuth.identityId,
identityAccessTokenId: identityAccessToken.id,
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN,
identityAuth: {
kubernetes: {
namespace: targetNamespace,
name: targetName
}
}
} as TIdentityAccessTokenJwtPayload,
appCfg.AUTH_SECRET,
// akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error
Number(identityAccessToken.accessTokenTTL) === 0
? undefined
: {
expiresIn: Number(identityAccessToken.accessTokenTTL)
}
);
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
authAttemptCounter.add(1, {
"infisical.identity.id": identityKubernetesAuth.identityId,
"infisical.identity.name": identity.name,
"infisical.organization.id": org.id,
"infisical.organization.name": org.name,
"infisical.identity.auth_method": AuthAttemptAuthMethod.KUBERNETES_AUTH,
"infisical.identity.auth_result": AuthAttemptAuthResult.SUCCESS,
"client.address": requestContext.get("ip"),
"user_agent.original": requestContext.get("userAgent")
});
}
return { accessToken, identityKubernetesAuth, identityAccessToken, identity };
return { accessToken, identityKubernetesAuth, identityAccessToken, identity };
} catch (error) {
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
authAttemptCounter.add(1, {
"infisical.identity.id": identityKubernetesAuth.identityId,
"infisical.identity.name": identity.name,
"infisical.organization.id": org.id,
"infisical.organization.name": org.name,
"infisical.identity.auth_method": AuthAttemptAuthMethod.KUBERNETES_AUTH,
"infisical.identity.auth_result": AuthAttemptAuthResult.FAILURE,
"client.address": requestContext.get("ip"),
"user_agent.original": requestContext.get("userAgent")
});
}
throw error;
}
};
const attachKubernetesAuth = async ({

View File

@@ -1,5 +1,6 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { ForbiddenError } from "@casl/ability";
import { requestContext } from "@fastify/request-context";
import slugify from "@sindresorhus/slugify";
import { AccessScope, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas";
@@ -29,6 +30,7 @@ import {
} from "@app/lib/errors";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
import { logger } from "@app/lib/logger";
import { AuthAttemptAuthMethod, AuthAttemptAuthResult, authAttemptCounter } from "@app/lib/telemetry/metrics";
import { ActorType, AuthTokenType } from "../auth/auth-type";
import { TIdentityDALFactory } from "../identity/identity-dal";
@@ -151,6 +153,7 @@ export const identityLdapAuthServiceFactory = ({
};
const login = async ({ identityId }: TLoginLdapAuthDTO) => {
const appCfg = getConfig();
const identityLdapAuth = await identityLdapAuthDAL.findOne({ identityId });
if (!identityLdapAuth) {
@@ -162,6 +165,7 @@ export const identityLdapAuthServiceFactory = ({
const identity = await identityDAL.findById(identityLdapAuth.identityId);
if (!identity) throw new UnauthorizedError({ message: "Identity not found" });
const org = await orgDAL.findById(identity.orgId);
const plan = await licenseService.getPlan(identity.orgId);
if (!plan.ldap) {
throw new BadRequestError({
@@ -170,44 +174,72 @@ export const identityLdapAuthServiceFactory = ({
});
}
const identityAccessToken = await identityLdapAuthDAL.transaction(async (tx) => {
await membershipIdentityDAL.update(
{ scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id },
{ lastLoginAuthMethod: IdentityAuthMethod.LDAP_AUTH, lastLoginTime: new Date() },
tx
);
const newToken = await identityAccessTokenDAL.create(
try {
const identityAccessToken = await identityLdapAuthDAL.transaction(async (tx) => {
await membershipIdentityDAL.update(
{ scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id },
{ lastLoginAuthMethod: IdentityAuthMethod.LDAP_AUTH, lastLoginTime: new Date() },
tx
);
const newToken = await identityAccessTokenDAL.create(
{
identityId: identityLdapAuth.identityId,
isAccessTokenRevoked: false,
accessTokenTTL: identityLdapAuth.accessTokenTTL,
accessTokenMaxTTL: identityLdapAuth.accessTokenMaxTTL,
accessTokenNumUses: 0,
accessTokenNumUsesLimit: identityLdapAuth.accessTokenNumUsesLimit,
authMethod: IdentityAuthMethod.LDAP_AUTH
},
tx
);
return newToken;
});
const accessToken = crypto.jwt().sign(
{
identityId: identityLdapAuth.identityId,
isAccessTokenRevoked: false,
accessTokenTTL: identityLdapAuth.accessTokenTTL,
accessTokenMaxTTL: identityLdapAuth.accessTokenMaxTTL,
accessTokenNumUses: 0,
accessTokenNumUsesLimit: identityLdapAuth.accessTokenNumUsesLimit,
authMethod: IdentityAuthMethod.LDAP_AUTH
},
tx
identityAccessTokenId: identityAccessToken.id,
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
} as TIdentityAccessTokenJwtPayload,
appCfg.AUTH_SECRET,
// akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error
Number(identityAccessToken.accessTokenTTL) === 0
? undefined
: {
expiresIn: Number(identityAccessToken.accessTokenTTL)
}
);
return newToken;
});
const appCfg = getConfig();
const accessToken = crypto.jwt().sign(
{
identityId: identityLdapAuth.identityId,
identityAccessTokenId: identityAccessToken.id,
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
} as TIdentityAccessTokenJwtPayload,
appCfg.AUTH_SECRET,
// akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error
Number(identityAccessToken.accessTokenTTL) === 0
? undefined
: {
expiresIn: Number(identityAccessToken.accessTokenTTL)
}
);
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
authAttemptCounter.add(1, {
"infisical.identity.id": identityLdapAuth.identityId,
"infisical.identity.name": identity.name,
"infisical.organization.id": org.id,
"infisical.organization.name": org.name,
"infisical.identity.auth_method": AuthAttemptAuthMethod.LDAP_AUTH,
"infisical.identity.auth_result": AuthAttemptAuthResult.SUCCESS,
"client.address": requestContext.get("ip"),
"user_agent.original": requestContext.get("userAgent")
});
}
return { accessToken, identityLdapAuth, identityAccessToken, identity };
return { accessToken, identityLdapAuth, identityAccessToken, identity };
} catch (error) {
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
authAttemptCounter.add(1, {
"infisical.identity.id": identityLdapAuth.identityId,
"infisical.identity.name": identity.name,
"infisical.organization.id": org.id,
"infisical.organization.name": org.name,
"infisical.identity.auth_method": AuthAttemptAuthMethod.LDAP_AUTH,
"infisical.identity.auth_result": AuthAttemptAuthResult.FAILURE,
"client.address": requestContext.get("ip"),
"user_agent.original": requestContext.get("userAgent")
});
}
throw error;
}
};
const attachLdapAuth = async ({

View File

@@ -1,5 +1,6 @@
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { ForbiddenError } from "@casl/ability";
import { requestContext } from "@fastify/request-context";
import { AxiosError } from "axios";
import RE2 from "re2";
@@ -23,6 +24,7 @@ import {
} from "@app/lib/errors";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
import { logger } from "@app/lib/logger";
import { AuthAttemptAuthMethod, AuthAttemptAuthResult, authAttemptCounter } from "@app/lib/telemetry/metrics";
import { ActorType, AuthTokenType } from "../auth/auth-type";
import { TIdentityDALFactory } from "../identity/identity-dal";
@@ -63,6 +65,7 @@ export const identityOciAuthServiceFactory = ({
orgDAL
}: TIdentityOciAuthServiceFactoryDep) => {
const login = async ({ identityId, headers, userOcid }: TLoginOciAuthDTO) => {
const appCfg = getConfig();
const identityOciAuth = await identityOciAuthDAL.findOne({ identityId });
if (!identityOciAuth) {
throw new NotFoundError({ message: "OCI auth method not found for identity, did you configure OCI auth?" });
@@ -71,80 +74,109 @@ export const identityOciAuthServiceFactory = ({
const identity = await identityDAL.findById(identityOciAuth.identityId);
if (!identity) throw new UnauthorizedError({ message: "Identity not found" });
// Validate OCI host format. Ensures that the host is in "identity.<region>.oraclecloud.com" format.
if (!headers.host || !new RE2("^identity\\.([a-z]{2}-[a-z]+-[1-9])\\.oraclecloud\\.com$").test(headers.host)) {
throw new BadRequestError({
message: "Invalid OCI host format. Expected format: identity.<region>.oraclecloud.com"
});
}
const { data } = await request
.get<TOciGetUserResponse>(`https://${headers.host}/20160918/users/${userOcid}`, {
headers
})
.catch((err: AxiosError) => {
logger.error(err.response, "OciIdentityLogin: Failed to authenticate with Oracle Cloud");
throw err;
});
if (data.compartmentId !== identityOciAuth.tenancyOcid) {
throw new UnauthorizedError({
message: "Access denied: OCI account isn't part of tenancy."
});
}
if (identityOciAuth.allowedUsernames) {
const isAccountAllowed = identityOciAuth.allowedUsernames.split(",").some((name) => name.trim() === data.name);
if (!isAccountAllowed)
throw new UnauthorizedError({
message: "Access denied: OCI account username not allowed."
const org = await orgDAL.findById(identity.orgId);
try {
// Validate OCI host format. Ensures that the host is in "identity.<region>.oraclecloud.com" format.
if (!headers.host || !new RE2("^identity\\.([a-z]{2}-[a-z]+-[1-9])\\.oraclecloud\\.com$").test(headers.host)) {
throw new BadRequestError({
message: "Invalid OCI host format. Expected format: identity.<region>.oraclecloud.com"
});
}
}
// Generate the token
const identityAccessToken = await identityOciAuthDAL.transaction(async (tx) => {
await membershipIdentityDAL.update(
{ scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id },
{ lastLoginAuthMethod: IdentityAuthMethod.OCI_AUTH, lastLoginTime: new Date() },
tx
);
const newToken = await identityAccessTokenDAL.create(
const { data } = await request
.get<TOciGetUserResponse>(`https://${headers.host}/20160918/users/${userOcid}`, {
headers
})
.catch((err: AxiosError) => {
logger.error(err.response, "OciIdentityLogin: Failed to authenticate with Oracle Cloud");
throw err;
});
if (data.compartmentId !== identityOciAuth.tenancyOcid) {
throw new UnauthorizedError({
message: "Access denied: OCI account isn't part of tenancy."
});
}
if (identityOciAuth.allowedUsernames) {
const isAccountAllowed = identityOciAuth.allowedUsernames.split(",").some((name) => name.trim() === data.name);
if (!isAccountAllowed)
throw new UnauthorizedError({
message: "Access denied: OCI account username not allowed."
});
}
// Generate the token
const identityAccessToken = await identityOciAuthDAL.transaction(async (tx) => {
await membershipIdentityDAL.update(
{ scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id },
{ lastLoginAuthMethod: IdentityAuthMethod.OCI_AUTH, lastLoginTime: new Date() },
tx
);
const newToken = await identityAccessTokenDAL.create(
{
identityId: identityOciAuth.identityId,
isAccessTokenRevoked: false,
accessTokenTTL: identityOciAuth.accessTokenTTL,
accessTokenMaxTTL: identityOciAuth.accessTokenMaxTTL,
accessTokenNumUses: 0,
accessTokenNumUsesLimit: identityOciAuth.accessTokenNumUsesLimit,
authMethod: IdentityAuthMethod.OCI_AUTH
},
tx
);
return newToken;
});
const accessToken = crypto.jwt().sign(
{
identityId: identityOciAuth.identityId,
isAccessTokenRevoked: false,
accessTokenTTL: identityOciAuth.accessTokenTTL,
accessTokenMaxTTL: identityOciAuth.accessTokenMaxTTL,
accessTokenNumUses: 0,
accessTokenNumUsesLimit: identityOciAuth.accessTokenNumUsesLimit,
authMethod: IdentityAuthMethod.OCI_AUTH
},
tx
identityAccessTokenId: identityAccessToken.id,
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
} as TIdentityAccessTokenJwtPayload,
appCfg.AUTH_SECRET,
Number(identityAccessToken.accessTokenTTL) === 0
? undefined
: {
expiresIn: Number(identityAccessToken.accessTokenTTL)
}
);
return newToken;
});
const appCfg = getConfig();
const accessToken = crypto.jwt().sign(
{
identityId: identityOciAuth.identityId,
identityAccessTokenId: identityAccessToken.id,
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
} as TIdentityAccessTokenJwtPayload,
appCfg.AUTH_SECRET,
Number(identityAccessToken.accessTokenTTL) === 0
? undefined
: {
expiresIn: Number(identityAccessToken.accessTokenTTL)
}
);
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
authAttemptCounter.add(1, {
"infisical.identity.id": identityOciAuth.identityId,
"infisical.identity.name": identity.name,
"infisical.organization.id": org.id,
"infisical.organization.name": org.name,
"infisical.identity.auth_method": AuthAttemptAuthMethod.OCI_AUTH,
"infisical.identity.auth_result": AuthAttemptAuthResult.SUCCESS,
"client.address": requestContext.get("ip"),
"user_agent.original": requestContext.get("userAgent")
});
}
return {
identityOciAuth,
accessToken,
identityAccessToken,
identity
};
return {
identityOciAuth,
accessToken,
identityAccessToken,
identity
};
} catch (error) {
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
authAttemptCounter.add(1, {
"infisical.identity.id": identityOciAuth.identityId,
"infisical.identity.name": identity.name,
"infisical.organization.id": org.id,
"infisical.organization.name": org.name,
"infisical.identity.auth_method": AuthAttemptAuthMethod.OCI_AUTH,
"infisical.identity.auth_result": AuthAttemptAuthResult.FAILURE,
"client.address": requestContext.get("ip"),
"user_agent.original": requestContext.get("userAgent")
});
}
throw error;
}
};
const attachOciAuth = async ({

View File

@@ -1,4 +1,5 @@
import { ForbiddenError } from "@casl/ability";
import { requestContext } from "@fastify/request-context";
import axios from "axios";
import https from "https";
import jwt from "jsonwebtoken";
@@ -22,6 +23,7 @@ import {
UnauthorizedError
} from "@app/lib/errors";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
import { AuthAttemptAuthMethod, AuthAttemptAuthResult, authAttemptCounter } from "@app/lib/telemetry/metrics";
import { getValueByDot } from "@app/lib/template/dot-access";
import { ActorType, AuthTokenType } from "../auth/auth-type";
@@ -67,6 +69,7 @@ export const identityOidcAuthServiceFactory = ({
orgDAL
}: TIdentityOidcAuthServiceFactoryDep) => {
const login = async ({ identityId, jwt: oidcJwt }: TLoginOidcAuthDTO) => {
const appCfg = getConfig();
const identityOidcAuth = await identityOidcAuthDAL.findOne({ identityId });
if (!identityOidcAuth) {
throw new NotFoundError({ message: "OIDC auth method not found for identity, did you configure OIDC auth?" });
@@ -75,151 +78,180 @@ export const identityOidcAuthServiceFactory = ({
const identity = await identityDAL.findById(identityOidcAuth.identityId);
if (!identity) throw new UnauthorizedError({ message: "Identity not found" });
const { decryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization,
orgId: identity.orgId
});
let caCert = "";
if (identityOidcAuth.encryptedCaCertificate) {
caCert = decryptor({ cipherTextBlob: identityOidcAuth.encryptedCaCertificate }).toString();
}
const requestAgent = new https.Agent({ ca: caCert, rejectUnauthorized: !!caCert });
const { data: discoveryDoc } = await axios.get<{ jwks_uri: string }>(
`${identityOidcAuth.oidcDiscoveryUrl}/.well-known/openid-configuration`,
{
httpsAgent: identityOidcAuth.oidcDiscoveryUrl.includes("https") ? requestAgent : undefined
}
);
const jwksUri = discoveryDoc.jwks_uri;
const decodedToken = crypto.jwt().decode(oidcJwt, { complete: true });
if (!decodedToken) {
throw new UnauthorizedError({
message: "Invalid JWT"
});
}
const client = new JwksClient({
jwksUri,
requestAgent: identityOidcAuth.oidcDiscoveryUrl.includes("https") ? requestAgent : undefined
});
const { kid } = decodedToken.header as { kid: string };
const oidcSigningKey = await client.getSigningKey(kid);
let tokenData: Record<string, string>;
const org = await orgDAL.findById(identity.orgId);
try {
tokenData = crypto.jwt().verify(oidcJwt, oidcSigningKey.getPublicKey(), {
issuer: identityOidcAuth.boundIssuer
}) as Record<string, string>;
} catch (error) {
if (error instanceof jwt.JsonWebTokenError) {
const { decryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization,
orgId: identity.orgId
});
let caCert = "";
if (identityOidcAuth.encryptedCaCertificate) {
caCert = decryptor({ cipherTextBlob: identityOidcAuth.encryptedCaCertificate }).toString();
}
const requestAgent = new https.Agent({ ca: caCert, rejectUnauthorized: !!caCert });
const { data: discoveryDoc } = await axios.get<{ jwks_uri: string }>(
`${identityOidcAuth.oidcDiscoveryUrl}/.well-known/openid-configuration`,
{
httpsAgent: identityOidcAuth.oidcDiscoveryUrl.includes("https") ? requestAgent : undefined
}
);
const jwksUri = discoveryDoc.jwks_uri;
const decodedToken = crypto.jwt().decode(oidcJwt, { complete: true });
if (!decodedToken) {
throw new UnauthorizedError({
message: `Access denied: ${error.message}`
message: "Invalid JWT"
});
}
const client = new JwksClient({
jwksUri,
requestAgent: identityOidcAuth.oidcDiscoveryUrl.includes("https") ? requestAgent : undefined
});
const { kid } = decodedToken.header as { kid: string };
const oidcSigningKey = await client.getSigningKey(kid);
let tokenData: Record<string, string>;
try {
tokenData = crypto.jwt().verify(oidcJwt, oidcSigningKey.getPublicKey(), {
issuer: identityOidcAuth.boundIssuer
}) as Record<string, string>;
} catch (error) {
if (error instanceof jwt.JsonWebTokenError) {
throw new UnauthorizedError({
message: `Access denied: ${error.message}`
});
}
throw error;
}
if (identityOidcAuth.boundSubject) {
if (!doesFieldValueMatchOidcPolicy(tokenData.sub, identityOidcAuth.boundSubject)) {
throw new ForbiddenRequestError({
message: "Access denied: OIDC subject not allowed."
});
}
}
if (identityOidcAuth.boundAudiences) {
if (
!identityOidcAuth.boundAudiences
.split(", ")
.some((policyValue) => doesAudValueMatchOidcPolicy(tokenData.aud, policyValue))
) {
throw new UnauthorizedError({
message: "Access denied: OIDC audience not allowed."
});
}
}
if (identityOidcAuth.boundClaims) {
Object.keys(identityOidcAuth.boundClaims).forEach((claimKey) => {
const claimValue = (identityOidcAuth.boundClaims as Record<string, string>)[claimKey];
const value = getValueByDot(tokenData, claimKey);
if (!value) {
throw new UnauthorizedError({
message: `Access denied: token has no ${claimKey} field`
});
}
// handle both single and multi-valued claims
if (!claimValue.split(", ").some((claimEntry) => doesFieldValueMatchOidcPolicy(value, claimEntry))) {
throw new UnauthorizedError({
message: "Access denied: OIDC claim not allowed."
});
}
});
}
const filteredClaims: Record<string, string> = {};
if (identityOidcAuth.claimMetadataMapping) {
Object.keys(identityOidcAuth.claimMetadataMapping).forEach((permissionKey) => {
const claimKey = (identityOidcAuth.claimMetadataMapping as Record<string, string>)[permissionKey];
const value = getValueByDot(tokenData, claimKey);
if (!value) {
throw new UnauthorizedError({
message: `Access denied: token has no ${claimKey} field`
});
}
filteredClaims[permissionKey] = value.toString();
});
}
const identityAccessToken = await identityOidcAuthDAL.transaction(async (tx) => {
await membershipIdentityDAL.update(
{ scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id },
{ lastLoginAuthMethod: IdentityAuthMethod.OIDC_AUTH, lastLoginTime: new Date() },
tx
);
const newToken = await identityAccessTokenDAL.create(
{
identityId: identityOidcAuth.identityId,
isAccessTokenRevoked: false,
accessTokenTTL: identityOidcAuth.accessTokenTTL,
accessTokenMaxTTL: identityOidcAuth.accessTokenMaxTTL,
accessTokenNumUses: 0,
accessTokenNumUsesLimit: identityOidcAuth.accessTokenNumUsesLimit,
authMethod: IdentityAuthMethod.OIDC_AUTH
},
tx
);
return newToken;
});
const accessToken = crypto.jwt().sign(
{
identityId: identityOidcAuth.identityId,
identityAccessTokenId: identityAccessToken.id,
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN,
identityAuth: {
oidc: {
claims: filteredClaims
}
}
} as TIdentityAccessTokenJwtPayload,
appCfg.AUTH_SECRET,
// akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error
Number(identityAccessToken.accessTokenTTL) === 0
? undefined
: {
expiresIn: Number(identityAccessToken.accessTokenTTL)
}
);
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
authAttemptCounter.add(1, {
"infisical.identity.id": identityOidcAuth.identityId,
"infisical.identity.name": identity.name,
"infisical.organization.id": org.id,
"infisical.organization.name": org.name,
"infisical.identity.auth_method": AuthAttemptAuthMethod.OIDC_AUTH,
"infisical.identity.auth_result": AuthAttemptAuthResult.SUCCESS,
"client.address": requestContext.get("ip"),
"user_agent.original": requestContext.get("userAgent")
});
}
return { accessToken, identityOidcAuth, identityAccessToken, identity, oidcTokenData: tokenData };
} catch (error) {
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
authAttemptCounter.add(1, {
"infisical.identity.id": identityOidcAuth.identityId,
"infisical.identity.name": identity.name,
"infisical.organization.id": org.id,
"infisical.organization.name": org.name,
"infisical.identity.auth_method": AuthAttemptAuthMethod.OIDC_AUTH,
"infisical.identity.auth_result": AuthAttemptAuthResult.FAILURE,
"client.address": requestContext.get("ip"),
"user_agent.original": requestContext.get("userAgent")
});
}
throw error;
}
if (identityOidcAuth.boundSubject) {
if (!doesFieldValueMatchOidcPolicy(tokenData.sub, identityOidcAuth.boundSubject)) {
throw new ForbiddenRequestError({
message: "Access denied: OIDC subject not allowed."
});
}
}
if (identityOidcAuth.boundAudiences) {
if (
!identityOidcAuth.boundAudiences
.split(", ")
.some((policyValue) => doesAudValueMatchOidcPolicy(tokenData.aud, policyValue))
) {
throw new UnauthorizedError({
message: "Access denied: OIDC audience not allowed."
});
}
}
if (identityOidcAuth.boundClaims) {
Object.keys(identityOidcAuth.boundClaims).forEach((claimKey) => {
const claimValue = (identityOidcAuth.boundClaims as Record<string, string>)[claimKey];
const value = getValueByDot(tokenData, claimKey);
if (!value) {
throw new UnauthorizedError({
message: `Access denied: token has no ${claimKey} field`
});
}
// handle both single and multi-valued claims
if (!claimValue.split(", ").some((claimEntry) => doesFieldValueMatchOidcPolicy(value, claimEntry))) {
throw new UnauthorizedError({
message: "Access denied: OIDC claim not allowed."
});
}
});
}
const filteredClaims: Record<string, string> = {};
if (identityOidcAuth.claimMetadataMapping) {
Object.keys(identityOidcAuth.claimMetadataMapping).forEach((permissionKey) => {
const claimKey = (identityOidcAuth.claimMetadataMapping as Record<string, string>)[permissionKey];
const value = getValueByDot(tokenData, claimKey);
if (!value) {
throw new UnauthorizedError({
message: `Access denied: token has no ${claimKey} field`
});
}
filteredClaims[permissionKey] = value.toString();
});
}
const identityAccessToken = await identityOidcAuthDAL.transaction(async (tx) => {
await membershipIdentityDAL.update(
{ scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id },
{ lastLoginAuthMethod: IdentityAuthMethod.OIDC_AUTH, lastLoginTime: new Date() },
tx
);
const newToken = await identityAccessTokenDAL.create(
{
identityId: identityOidcAuth.identityId,
isAccessTokenRevoked: false,
accessTokenTTL: identityOidcAuth.accessTokenTTL,
accessTokenMaxTTL: identityOidcAuth.accessTokenMaxTTL,
accessTokenNumUses: 0,
accessTokenNumUsesLimit: identityOidcAuth.accessTokenNumUsesLimit,
authMethod: IdentityAuthMethod.OIDC_AUTH
},
tx
);
return newToken;
});
const appCfg = getConfig();
const accessToken = crypto.jwt().sign(
{
identityId: identityOidcAuth.identityId,
identityAccessTokenId: identityAccessToken.id,
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN,
identityAuth: {
oidc: {
claims: filteredClaims
}
}
} as TIdentityAccessTokenJwtPayload,
appCfg.AUTH_SECRET,
// akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error
Number(identityAccessToken.accessTokenTTL) === 0
? undefined
: {
expiresIn: Number(identityAccessToken.accessTokenTTL)
}
);
return { accessToken, identityOidcAuth, identityAccessToken, identity, oidcTokenData: tokenData };
};
const attachOidcAuth = async ({

View File

@@ -1,4 +1,5 @@
import { ForbiddenError } from "@casl/ability";
import { requestContext } from "@fastify/request-context";
import { AccessScope, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
@@ -19,6 +20,7 @@ import {
UnauthorizedError
} from "@app/lib/errors";
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
import { AuthAttemptAuthMethod, AuthAttemptAuthResult, authAttemptCounter } from "@app/lib/telemetry/metrics";
import { ActorType, AuthTokenType } from "../auth/auth-type";
import { TIdentityDALFactory } from "../identity/identity-dal";
@@ -27,6 +29,7 @@ import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identit
import { TKmsServiceFactory } from "../kms/kms-service";
import { KmsDataKey } from "../kms/kms-types";
import { TMembershipIdentityDALFactory } from "../membership-identity/membership-identity-dal";
import { TOrgDALFactory } from "../org/org-dal";
import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns";
import { TIdentityTlsCertAuthDALFactory } from "./identity-tls-cert-auth-dal";
import { TIdentityTlsCertAuthServiceFactory } from "./identity-tls-cert-auth-types";
@@ -42,6 +45,7 @@ type TIdentityTlsCertAuthServiceFactoryDep = {
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
orgDAL: Pick<TOrgDALFactory, "findById">;
};
const parseSubjectDetails = (data: string) => {
@@ -60,9 +64,11 @@ export const identityTlsCertAuthServiceFactory = ({
membershipIdentityDAL,
licenseService,
permissionService,
kmsService
kmsService,
orgDAL
}: TIdentityTlsCertAuthServiceFactoryDep): TIdentityTlsCertAuthServiceFactory => {
const login: TIdentityTlsCertAuthServiceFactory["login"] = async ({ identityId, clientCertificate }) => {
const appCfg = getConfig();
const identityTlsCertAuth = await identityTlsCertAuthDAL.findOne({ identityId });
if (!identityTlsCertAuth) {
throw new NotFoundError({
@@ -73,94 +79,124 @@ export const identityTlsCertAuthServiceFactory = ({
const identity = await identityDAL.findById(identityTlsCertAuth.identityId);
if (!identity) throw new UnauthorizedError({ message: "Identity not found" });
const { decryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization,
orgId: identity.orgId
});
const org = await orgDAL.findById(identity.orgId);
const caCertificate = decryptor({
cipherTextBlob: identityTlsCertAuth.encryptedCaCertificate
}).toString();
const leafCertificate = extractX509CertFromChain(decodeURIComponent(clientCertificate))?.[0];
if (!leafCertificate) {
throw new BadRequestError({ message: "Missing client certificate" });
}
const clientCertificateX509 = new crypto.nativeCrypto.X509Certificate(leafCertificate);
const caCertificateX509 = new crypto.nativeCrypto.X509Certificate(caCertificate);
const isValidCertificate = clientCertificateX509.verify(caCertificateX509.publicKey);
if (!isValidCertificate)
throw new UnauthorizedError({
message: "Access denied: Certificate not issued by the provided CA."
try {
const { decryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization,
orgId: identity.orgId
});
if (new Date(clientCertificateX509.validTo) < new Date()) {
throw new UnauthorizedError({
message: "Access denied: Certificate has expired."
});
}
const caCertificate = decryptor({
cipherTextBlob: identityTlsCertAuth.encryptedCaCertificate
}).toString();
if (new Date(clientCertificateX509.validFrom) > new Date()) {
throw new UnauthorizedError({
message: "Access denied: Certificate not yet valid."
});
}
const leafCertificate = extractX509CertFromChain(decodeURIComponent(clientCertificate))?.[0];
if (!leafCertificate) {
throw new BadRequestError({ message: "Missing client certificate" });
}
const subjectDetails = parseSubjectDetails(clientCertificateX509.subject);
if (identityTlsCertAuth.allowedCommonNames) {
const isValidCommonName = identityTlsCertAuth.allowedCommonNames.split(",").includes(subjectDetails.CN);
if (!isValidCommonName) {
const clientCertificateX509 = new crypto.nativeCrypto.X509Certificate(leafCertificate);
const caCertificateX509 = new crypto.nativeCrypto.X509Certificate(caCertificate);
const isValidCertificate = clientCertificateX509.verify(caCertificateX509.publicKey);
if (!isValidCertificate)
throw new UnauthorizedError({
message: "Access denied: TLS Certificate Auth common name not allowed."
message: "Access denied: Certificate not issued by the provided CA."
});
if (new Date(clientCertificateX509.validTo) < new Date()) {
throw new UnauthorizedError({
message: "Access denied: Certificate has expired."
});
}
}
// Generate the token
const identityAccessToken = await identityTlsCertAuthDAL.transaction(async (tx) => {
await membershipIdentityDAL.update(
{ scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id },
{ lastLoginAuthMethod: IdentityAuthMethod.TLS_CERT_AUTH, lastLoginTime: new Date() },
tx
);
const newToken = await identityAccessTokenDAL.create(
if (new Date(clientCertificateX509.validFrom) > new Date()) {
throw new UnauthorizedError({
message: "Access denied: Certificate not yet valid."
});
}
const subjectDetails = parseSubjectDetails(clientCertificateX509.subject);
if (identityTlsCertAuth.allowedCommonNames) {
const isValidCommonName = identityTlsCertAuth.allowedCommonNames.split(",").includes(subjectDetails.CN);
if (!isValidCommonName) {
throw new UnauthorizedError({
message: "Access denied: TLS Certificate Auth common name not allowed."
});
}
}
// Generate the token
const identityAccessToken = await identityTlsCertAuthDAL.transaction(async (tx) => {
await membershipIdentityDAL.update(
{ scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id },
{ lastLoginAuthMethod: IdentityAuthMethod.TLS_CERT_AUTH, lastLoginTime: new Date() },
tx
);
const newToken = await identityAccessTokenDAL.create(
{
identityId: identityTlsCertAuth.identityId,
isAccessTokenRevoked: false,
accessTokenTTL: identityTlsCertAuth.accessTokenTTL,
accessTokenMaxTTL: identityTlsCertAuth.accessTokenMaxTTL,
accessTokenNumUses: 0,
accessTokenNumUsesLimit: identityTlsCertAuth.accessTokenNumUsesLimit,
authMethod: IdentityAuthMethod.TLS_CERT_AUTH
},
tx
);
return newToken;
});
const accessToken = crypto.jwt().sign(
{
identityId: identityTlsCertAuth.identityId,
isAccessTokenRevoked: false,
accessTokenTTL: identityTlsCertAuth.accessTokenTTL,
accessTokenMaxTTL: identityTlsCertAuth.accessTokenMaxTTL,
accessTokenNumUses: 0,
accessTokenNumUsesLimit: identityTlsCertAuth.accessTokenNumUsesLimit,
authMethod: IdentityAuthMethod.TLS_CERT_AUTH
},
tx
identityAccessTokenId: identityAccessToken.id,
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
} as TIdentityAccessTokenJwtPayload,
appCfg.AUTH_SECRET,
Number(identityAccessToken.accessTokenTTL) === 0
? undefined
: {
expiresIn: Number(identityAccessToken.accessTokenTTL)
}
);
return newToken;
});
const appCfg = getConfig();
const accessToken = crypto.jwt().sign(
{
identityId: identityTlsCertAuth.identityId,
identityAccessTokenId: identityAccessToken.id,
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
} as TIdentityAccessTokenJwtPayload,
appCfg.AUTH_SECRET,
Number(identityAccessToken.accessTokenTTL) === 0
? undefined
: {
expiresIn: Number(identityAccessToken.accessTokenTTL)
}
);
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
authAttemptCounter.add(1, {
"infisical.identity.id": identityTlsCertAuth.identityId,
"infisical.identity.name": identity.name,
"infisical.organization.id": org.id,
"infisical.organization.name": org.name,
"infisical.identity.auth_method": AuthAttemptAuthMethod.TLS_CERT_AUTH,
"infisical.identity.auth_result": AuthAttemptAuthResult.SUCCESS,
"client.address": requestContext.get("ip"),
"user_agent.original": requestContext.get("userAgent")
});
}
return {
identityTlsCertAuth,
accessToken,
identityAccessToken,
identity
};
return {
identityTlsCertAuth,
accessToken,
identityAccessToken,
identity
};
} catch (error) {
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
authAttemptCounter.add(1, {
"infisical.identity.id": identityTlsCertAuth.identityId,
"infisical.identity.name": identity.name,
"infisical.organization.id": org.id,
"infisical.organization.name": org.name,
"infisical.identity.auth_method": AuthAttemptAuthMethod.TLS_CERT_AUTH,
"infisical.identity.auth_result": AuthAttemptAuthResult.FAILURE,
"client.address": requestContext.get("ip"),
"user_agent.original": requestContext.get("userAgent")
});
}
throw error;
}
};
const attachTlsCertAuth: TIdentityTlsCertAuthServiceFactory["attachTlsCertAuth"] = async ({

View File

@@ -1,4 +1,5 @@
import { ForbiddenError } from "@casl/ability";
import { requestContext } from "@fastify/request-context";
import { AccessScope, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
@@ -21,6 +22,7 @@ import {
} from "@app/lib/errors";
import { checkIPAgainstBlocklist, extractIPDetails, isValidIpOrCidr, TIp } from "@app/lib/ip";
import { logger } from "@app/lib/logger";
import { AuthAttemptAuthMethod, AuthAttemptAuthResult, authAttemptCounter } from "@app/lib/telemetry/metrics";
import { ActorType, AuthTokenType } from "../auth/auth-type";
import { TIdentityDALFactory } from "../identity/identity-dal";
@@ -77,6 +79,7 @@ export const identityUaServiceFactory = ({
identityDAL
}: TIdentityUaServiceFactoryDep) => {
const login = async (clientId: string, clientSecret: string, ip: string) => {
const appCfg = getConfig();
const identityUa = await identityUaDAL.findOne({ clientId });
if (!identityUa) {
throw new UnauthorizedError({
@@ -84,196 +87,226 @@ export const identityUaServiceFactory = ({
});
}
checkIPAgainstBlocklist({
ipAddress: ip,
trustedIps: identityUa.clientSecretTrustedIps as TIp[]
});
const identity = await identityDAL.findById(identityUa.identityId);
const org = await orgDAL.findById(identity.orgId);
const LOCKOUT_KEY = `lockout:identity:${identityUa.identityId}:${IdentityAuthMethod.UNIVERSAL_AUTH}:${clientId}`;
const lockoutRaw = await keyStore.getItem(LOCKOUT_KEY);
let lockout: LockoutObject | undefined;
if (lockoutRaw) {
lockout = JSON.parse(lockoutRaw) as LockoutObject;
}
if (lockout && lockout.lockedOut) {
throw new UnauthorizedError({
message: "This identity auth method is temporarily locked, please try again later"
try {
checkIPAgainstBlocklist({
ipAddress: ip,
trustedIps: identityUa.clientSecretTrustedIps as TIp[]
});
}
const clientSecretPrefix = clientSecret.slice(0, 4);
const clientSecretInfo = await identityUaClientSecretDAL.find({
identityUAId: identityUa.id,
isClientSecretRevoked: false,
clientSecretPrefix
});
const LOCKOUT_KEY = `lockout:identity:${identityUa.identityId}:${IdentityAuthMethod.UNIVERSAL_AUTH}:${clientId}`;
let validClientSecretInfo: (typeof clientSecretInfo)[0] | null = null;
for await (const info of clientSecretInfo) {
const isMatch = await crypto.hashing().compareHash(clientSecret, info.clientSecretHash);
const lockoutRaw = await keyStore.getItem(LOCKOUT_KEY);
if (isMatch) {
validClientSecretInfo = info;
break;
let lockout: LockoutObject | undefined;
if (lockoutRaw) {
lockout = JSON.parse(lockoutRaw) as LockoutObject;
}
}
if (!validClientSecretInfo) {
if (identityUa.lockoutEnabled) {
let lock: Awaited<ReturnType<typeof keyStore.acquireLock>> | undefined;
try {
lock = await keyStore.acquireLock([KeyStorePrefixes.IdentityLockoutLock(LOCKOUT_KEY)], 300, {
retryCount: 3,
retryDelay: 300,
retryJitter: 100
});
if (lockout && lockout.lockedOut) {
throw new UnauthorizedError({
message: "This identity auth method is temporarily locked, please try again later"
});
}
// Re-fetch the latest lockout data while holding the lock
const lockoutRawNew = await keyStore.getItem(LOCKOUT_KEY);
if (lockoutRawNew) {
lockout = JSON.parse(lockoutRawNew) as LockoutObject;
} else {
lockout = {
lockedOut: false,
failedAttempts: 0
};
}
const clientSecretPrefix = clientSecret.slice(0, 4);
const clientSecretInfo = await identityUaClientSecretDAL.find({
identityUAId: identityUa.id,
isClientSecretRevoked: false,
clientSecretPrefix
});
if (lockout.lockedOut) {
throw new UnauthorizedError({
message: "This identity auth method is temporarily locked, please try again later"
});
}
let validClientSecretInfo: (typeof clientSecretInfo)[0] | null = null;
for await (const info of clientSecretInfo) {
const isMatch = await crypto.hashing().compareHash(clientSecret, info.clientSecretHash);
lockout.failedAttempts += 1;
if (lockout.failedAttempts >= identityUa.lockoutThreshold) {
lockout.lockedOut = true;
}
await keyStore.setItemWithExpiry(
LOCKOUT_KEY,
lockout.lockedOut ? identityUa.lockoutDurationSeconds : identityUa.lockoutCounterResetSeconds,
JSON.stringify(lockout)
);
} catch (e) {
if (lock === undefined) {
logger.info(
`identity login failed to acquire lock [identityId=${identityUa.identityId}] [authMethod=${IdentityAuthMethod.UNIVERSAL_AUTH}]`
);
throw new RateLimitError({ message: "Failed to acquire lock: rate limit exceeded" });
}
throw e;
} finally {
if (lock) {
await lock.release();
}
if (isMatch) {
validClientSecretInfo = info;
break;
}
}
throw new UnauthorizedError({ message: "Invalid credentials" });
} else if (lockout) {
// If credentials are valid, clear any existing lockout record
await keyStore.deleteItem(LOCKOUT_KEY);
}
if (!validClientSecretInfo) {
if (identityUa.lockoutEnabled) {
let lock: Awaited<ReturnType<typeof keyStore.acquireLock>> | undefined;
try {
lock = await keyStore.acquireLock([KeyStorePrefixes.IdentityLockoutLock(LOCKOUT_KEY)], 300, {
retryCount: 3,
retryDelay: 300,
retryJitter: 100
});
const { clientSecretTTL, clientSecretNumUses, clientSecretNumUsesLimit } = validClientSecretInfo;
if (Number(clientSecretTTL) > 0) {
const clientSecretCreated = new Date(validClientSecretInfo.createdAt);
const ttlInMilliseconds = Number(clientSecretTTL) * 1000;
const currentDate = new Date();
const expirationTime = new Date(clientSecretCreated.getTime() + ttlInMilliseconds);
// Re-fetch the latest lockout data while holding the lock
const lockoutRawNew = await keyStore.getItem(LOCKOUT_KEY);
if (lockoutRawNew) {
lockout = JSON.parse(lockoutRawNew) as LockoutObject;
} else {
lockout = {
lockedOut: false,
failedAttempts: 0
};
}
if (currentDate > expirationTime) {
if (lockout.lockedOut) {
throw new UnauthorizedError({
message: "This identity auth method is temporarily locked, please try again later"
});
}
lockout.failedAttempts += 1;
if (lockout.failedAttempts >= identityUa.lockoutThreshold) {
lockout.lockedOut = true;
}
await keyStore.setItemWithExpiry(
LOCKOUT_KEY,
lockout.lockedOut ? identityUa.lockoutDurationSeconds : identityUa.lockoutCounterResetSeconds,
JSON.stringify(lockout)
);
} catch (e) {
if (lock === undefined) {
logger.info(
`identity login failed to acquire lock [identityId=${identityUa.identityId}] [authMethod=${IdentityAuthMethod.UNIVERSAL_AUTH}]`
);
throw new RateLimitError({ message: "Failed to acquire lock: rate limit exceeded" });
}
throw e;
} finally {
if (lock) {
await lock.release();
}
}
}
throw new UnauthorizedError({ message: "Invalid credentials" });
} else if (lockout) {
// If credentials are valid, clear any existing lockout record
await keyStore.deleteItem(LOCKOUT_KEY);
}
const { clientSecretTTL, clientSecretNumUses, clientSecretNumUsesLimit } = validClientSecretInfo;
if (Number(clientSecretTTL) > 0) {
const clientSecretCreated = new Date(validClientSecretInfo.createdAt);
const ttlInMilliseconds = Number(clientSecretTTL) * 1000;
const currentDate = new Date();
const expirationTime = new Date(clientSecretCreated.getTime() + ttlInMilliseconds);
if (currentDate > expirationTime) {
await identityUaClientSecretDAL.updateById(validClientSecretInfo.id, {
isClientSecretRevoked: true
});
throw new UnauthorizedError({
message: "Access denied due to expired client secret"
});
}
}
if (clientSecretNumUsesLimit > 0 && clientSecretNumUses >= clientSecretNumUsesLimit) {
// number of times client secret can be used for
// a login operation reached
await identityUaClientSecretDAL.updateById(validClientSecretInfo.id, {
isClientSecretRevoked: true
});
throw new UnauthorizedError({
message: "Access denied due to expired client secret"
message: "Access denied due to client secret usage limit reached"
});
}
}
if (clientSecretNumUsesLimit > 0 && clientSecretNumUses >= clientSecretNumUsesLimit) {
// number of times client secret can be used for
// a login operation reached
await identityUaClientSecretDAL.updateById(validClientSecretInfo.id, {
isClientSecretRevoked: true
const accessTokenTTLParams =
Number(identityUa.accessTokenPeriod) === 0
? {
accessTokenTTL: identityUa.accessTokenTTL,
accessTokenMaxTTL: identityUa.accessTokenMaxTTL
}
: {
accessTokenTTL: identityUa.accessTokenPeriod,
// We set a very large Max TTL for periodic tokens to ensure that clients (even outdated ones) can always renew their token
// without them having to update their SDKs, CLIs, etc. This workaround sets it to 30 years to emulate "forever"
accessTokenMaxTTL: 1000000000
};
const identityAccessToken = await identityUaDAL.transaction(async (tx) => {
const uaClientSecretDoc = await identityUaClientSecretDAL.incrementUsage(validClientSecretInfo!.id, tx);
await membershipIdentityDAL.update(
{ scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id },
{
lastLoginAuthMethod: IdentityAuthMethod.UNIVERSAL_AUTH,
lastLoginTime: new Date()
},
tx
);
const newToken = await identityAccessTokenDAL.create(
{
identityId: identityUa.identityId,
isAccessTokenRevoked: false,
identityUAClientSecretId: uaClientSecretDoc.id,
accessTokenNumUses: 0,
accessTokenNumUsesLimit: identityUa.accessTokenNumUsesLimit,
accessTokenPeriod: identityUa.accessTokenPeriod,
authMethod: IdentityAuthMethod.UNIVERSAL_AUTH,
...accessTokenTTLParams
},
tx
);
return newToken;
});
throw new UnauthorizedError({
message: "Access denied due to client secret usage limit reached"
});
}
const accessTokenTTLParams =
Number(identityUa.accessTokenPeriod) === 0
? {
accessTokenTTL: identityUa.accessTokenTTL,
accessTokenMaxTTL: identityUa.accessTokenMaxTTL
}
: {
accessTokenTTL: identityUa.accessTokenPeriod,
// We set a very large Max TTL for periodic tokens to ensure that clients (even outdated ones) can always renew their token
// without them having to update their SDKs, CLIs, etc. This workaround sets it to 30 years to emulate "forever"
accessTokenMaxTTL: 1000000000
};
const identity = await identityDAL.findById(identityUa.identityId);
const identityAccessToken = await identityUaDAL.transaction(async (tx) => {
const uaClientSecretDoc = await identityUaClientSecretDAL.incrementUsage(validClientSecretInfo!.id, tx);
await membershipIdentityDAL.update(
{ scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id },
{
lastLoginAuthMethod: IdentityAuthMethod.UNIVERSAL_AUTH,
lastLoginTime: new Date()
},
tx
);
const newToken = await identityAccessTokenDAL.create(
const accessToken = crypto.jwt().sign(
{
identityId: identityUa.identityId,
isAccessTokenRevoked: false,
identityUAClientSecretId: uaClientSecretDoc.id,
accessTokenNumUses: 0,
accessTokenNumUsesLimit: identityUa.accessTokenNumUsesLimit,
accessTokenPeriod: identityUa.accessTokenPeriod,
authMethod: IdentityAuthMethod.UNIVERSAL_AUTH,
...accessTokenTTLParams
},
tx
clientSecretId: validClientSecretInfo.id,
identityAccessTokenId: identityAccessToken.id,
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
} as TIdentityAccessTokenJwtPayload,
appCfg.AUTH_SECRET,
// akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error
Number(identityAccessToken.accessTokenTTL) === 0
? undefined
: {
expiresIn: Number(identityAccessToken.accessTokenTTL)
}
);
return newToken;
});
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
authAttemptCounter.add(1, {
"infisical.identity.id": identityUa.identityId,
"infisical.identity.name": identity.name,
"infisical.organization.id": org.id,
"infisical.organization.name": org.name,
"infisical.identity.auth_method": AuthAttemptAuthMethod.UNIVERSAL_AUTH,
"infisical.identity.auth_result": AuthAttemptAuthResult.SUCCESS,
"client.address": requestContext.get("ip"),
"user_agent.original": requestContext.get("userAgent")
});
}
const appCfg = getConfig();
const accessToken = crypto.jwt().sign(
{
identityId: identityUa.identityId,
clientSecretId: validClientSecretInfo.id,
identityAccessTokenId: identityAccessToken.id,
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
} as TIdentityAccessTokenJwtPayload,
appCfg.AUTH_SECRET,
// akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error
Number(identityAccessToken.accessTokenTTL) === 0
? undefined
: {
expiresIn: Number(identityAccessToken.accessTokenTTL)
}
);
return {
accessToken,
identityUa,
validClientSecretInfo,
identityAccessToken,
identity,
...accessTokenTTLParams
};
return {
accessToken,
identityUa,
validClientSecretInfo,
identityAccessToken,
identity,
...accessTokenTTLParams
};
} catch (error) {
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
authAttemptCounter.add(1, {
"infisical.identity.id": identityUa.identityId,
"infisical.identity.name": identity.name,
"infisical.organization.id": org.id,
"infisical.organization.name": org.name,
"infisical.identity.auth_method": AuthAttemptAuthMethod.UNIVERSAL_AUTH,
"infisical.identity.auth_result": AuthAttemptAuthResult.FAILURE,
"client.address": requestContext.get("ip"),
"user_agent.original": requestContext.get("userAgent")
});
}
throw error;
}
};
const attachUniversalAuth = async ({