mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-11 05:28:51 +00:00
misc: add auth attempt metrics
This commit is contained in:
@@ -157,26 +157,30 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
|
|||||||
metadata: userMetadata
|
metadata: userMetadata
|
||||||
});
|
});
|
||||||
|
|
||||||
authAttemptCounter.add(1, {
|
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
|
||||||
"infisical.user.email": email.toLowerCase(),
|
authAttemptCounter.add(1, {
|
||||||
"infisical.user.id": user.id,
|
"infisical.user.email": email.toLowerCase(),
|
||||||
"infisical.organization.id": organization.id,
|
"infisical.user.id": user.id,
|
||||||
"infisical.organization.name": organization.name,
|
"infisical.organization.id": organization.id,
|
||||||
"infisical.auth.method": AuthAttemptAuthMethod.SAML,
|
"infisical.organization.name": organization.name,
|
||||||
"infisical.auth.result": AuthAttemptAuthResult.SUCCESS,
|
"infisical.auth.method": AuthAttemptAuthMethod.SAML,
|
||||||
"client.address": requestContext.get("ip"),
|
"infisical.auth.result": AuthAttemptAuthResult.SUCCESS,
|
||||||
"user_agent.original": requestContext.get("userAgent")
|
"client.address": requestContext.get("ip"),
|
||||||
});
|
"user_agent.original": requestContext.get("userAgent")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
cb(null, { isUserCompleted, providerAuthToken });
|
cb(null, { isUserCompleted, providerAuthToken });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
authAttemptCounter.add(1, {
|
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
|
||||||
"infisical.user.email": email.toLowerCase(),
|
authAttemptCounter.add(1, {
|
||||||
"infisical.auth.method": AuthAttemptAuthMethod.SAML,
|
"infisical.user.email": email.toLowerCase(),
|
||||||
"infisical.auth.result": AuthAttemptAuthResult.FAILURE,
|
"infisical.auth.method": AuthAttemptAuthMethod.SAML,
|
||||||
"client.address": requestContext.get("ip"),
|
"infisical.auth.result": AuthAttemptAuthResult.FAILURE,
|
||||||
"user_agent.original": requestContext.get("userAgent")
|
"client.address": requestContext.get("ip"),
|
||||||
});
|
"user_agent.original": requestContext.get("userAgent")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
logger.error(error);
|
logger.error(error);
|
||||||
cb(error as Error);
|
cb(error as Error);
|
||||||
|
|||||||
@@ -757,29 +757,33 @@ export const oidcConfigServiceFactory = ({
|
|||||||
manageGroupMemberships: oidcCfg.manageGroupMemberships
|
manageGroupMemberships: oidcCfg.manageGroupMemberships
|
||||||
})
|
})
|
||||||
.then(({ isUserCompleted, providerAuthToken, user }) => {
|
.then(({ isUserCompleted, providerAuthToken, user }) => {
|
||||||
authAttemptCounter.add(1, {
|
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
|
||||||
"infisical.user.email": claims?.email?.toLowerCase(),
|
authAttemptCounter.add(1, {
|
||||||
"infisical.user.id": user.id,
|
"infisical.user.email": claims?.email?.toLowerCase(),
|
||||||
"infisical.organization.id": org.id,
|
"infisical.user.id": user.id,
|
||||||
"infisical.organization.name": org.name,
|
"infisical.organization.id": org.id,
|
||||||
"infisical.auth.method": AuthAttemptAuthMethod.OIDC,
|
"infisical.organization.name": org.name,
|
||||||
"infisical.auth.result": AuthAttemptAuthResult.SUCCESS,
|
"infisical.auth.method": AuthAttemptAuthMethod.OIDC,
|
||||||
"client.address": requestContext.get("ip"),
|
"infisical.auth.result": AuthAttemptAuthResult.SUCCESS,
|
||||||
"user_agent.original": requestContext.get("userAgent")
|
"client.address": requestContext.get("ip"),
|
||||||
});
|
"user_agent.original": requestContext.get("userAgent")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
cb(null, { isUserCompleted, providerAuthToken });
|
cb(null, { isUserCompleted, providerAuthToken });
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
authAttemptCounter.add(1, {
|
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
|
||||||
"infisical.user.email": claims?.email?.toLowerCase(),
|
authAttemptCounter.add(1, {
|
||||||
"infisical.organization.id": org.id,
|
"infisical.user.email": claims?.email?.toLowerCase(),
|
||||||
"infisical.organization.name": org.name,
|
"infisical.organization.id": org.id,
|
||||||
"infisical.auth.method": AuthAttemptAuthMethod.OIDC,
|
"infisical.organization.name": org.name,
|
||||||
"infisical.auth.result": AuthAttemptAuthResult.FAILURE,
|
"infisical.auth.method": AuthAttemptAuthMethod.OIDC,
|
||||||
"client.address": requestContext.get("ip"),
|
"infisical.auth.result": AuthAttemptAuthResult.FAILURE,
|
||||||
"user_agent.original": requestContext.get("userAgent")
|
"client.address": requestContext.get("ip"),
|
||||||
});
|
"user_agent.original": requestContext.get("userAgent")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
cb(error);
|
cb(error);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,7 +8,19 @@ export enum AuthAttemptAuthMethod {
|
|||||||
OIDC = "oidc",
|
OIDC = "oidc",
|
||||||
GOOGLE = "google",
|
GOOGLE = "google",
|
||||||
GITHUB = "github",
|
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 {
|
export enum AuthAttemptAuthResult {
|
||||||
|
|||||||
@@ -76,28 +76,31 @@ export const registerOauthMiddlewares = (server: FastifyZodProvider) => {
|
|||||||
orgSlug
|
orgSlug
|
||||||
});
|
});
|
||||||
|
|
||||||
authAttemptCounter.add(1, {
|
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
|
||||||
"infisical.user.email": email,
|
authAttemptCounter.add(1, {
|
||||||
"infisical.user.id": user.id,
|
"infisical.user.email": email,
|
||||||
"infisical.organization.id": orgId,
|
"infisical.user.id": user.id,
|
||||||
"infisical.organization.name": orgName,
|
"infisical.organization.id": orgId,
|
||||||
"infisical.auth.method": AuthAttemptAuthMethod.GOOGLE,
|
"infisical.organization.name": orgName,
|
||||||
"infisical.auth.result": AuthAttemptAuthResult.SUCCESS,
|
"infisical.auth.method": AuthAttemptAuthMethod.GOOGLE,
|
||||||
"client.address": requestContext.get("ip"),
|
"infisical.auth.result": AuthAttemptAuthResult.SUCCESS,
|
||||||
"user_agent.original": requestContext.get("userAgent")
|
"client.address": requestContext.get("ip"),
|
||||||
});
|
"user_agent.original": requestContext.get("userAgent")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
cb(null, { isUserCompleted, providerAuthToken });
|
cb(null, { isUserCompleted, providerAuthToken });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(error);
|
logger.error(error);
|
||||||
authAttemptCounter.add(1, {
|
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
|
||||||
"infisical.user.email": email,
|
authAttemptCounter.add(1, {
|
||||||
"infisical.auth.method": AuthAttemptAuthMethod.GOOGLE,
|
"infisical.user.email": email,
|
||||||
"infisical.auth.result": AuthAttemptAuthResult.FAILURE,
|
"infisical.auth.method": AuthAttemptAuthMethod.GOOGLE,
|
||||||
"client.address": requestContext.get("ip"),
|
"infisical.auth.result": AuthAttemptAuthResult.FAILURE,
|
||||||
"user_agent.original": requestContext.get("userAgent")
|
"client.address": requestContext.get("ip"),
|
||||||
});
|
"user_agent.original": requestContext.get("userAgent")
|
||||||
|
});
|
||||||
|
}
|
||||||
cb(error as Error, false);
|
cb(error as Error, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -144,27 +147,30 @@ export const registerOauthMiddlewares = (server: FastifyZodProvider) => {
|
|||||||
callbackPort
|
callbackPort
|
||||||
});
|
});
|
||||||
|
|
||||||
authAttemptCounter.add(1, {
|
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
|
||||||
"infisical.user.email": email,
|
authAttemptCounter.add(1, {
|
||||||
"infisical.user.id": user.id,
|
"infisical.user.email": email,
|
||||||
"infisical.organization.id": orgId,
|
"infisical.user.id": user.id,
|
||||||
"infisical.organization.name": orgName,
|
"infisical.organization.id": orgId,
|
||||||
"infisical.auth.method": AuthAttemptAuthMethod.GITHUB,
|
"infisical.organization.name": orgName,
|
||||||
"infisical.auth.result": AuthAttemptAuthResult.SUCCESS,
|
"infisical.auth.method": AuthAttemptAuthMethod.GITHUB,
|
||||||
"client.address": requestContext.get("ip"),
|
"infisical.auth.result": AuthAttemptAuthResult.SUCCESS,
|
||||||
"user_agent.original": requestContext.get("userAgent")
|
"client.address": requestContext.get("ip"),
|
||||||
});
|
"user_agent.original": requestContext.get("userAgent")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
done(null, { isUserCompleted, providerAuthToken, externalProviderAccessToken: accessToken });
|
done(null, { isUserCompleted, providerAuthToken, externalProviderAccessToken: accessToken });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
authAttemptCounter.add(1, {
|
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
|
||||||
"infisical.user.email": email,
|
authAttemptCounter.add(1, {
|
||||||
"infisical.auth.method": AuthAttemptAuthMethod.GITHUB,
|
"infisical.user.email": email,
|
||||||
"infisical.auth.result": AuthAttemptAuthResult.FAILURE,
|
"infisical.auth.method": AuthAttemptAuthMethod.GITHUB,
|
||||||
"client.address": requestContext.get("ip"),
|
"infisical.auth.result": AuthAttemptAuthResult.FAILURE,
|
||||||
"user_agent.original": requestContext.get("userAgent")
|
"client.address": requestContext.get("ip"),
|
||||||
});
|
"user_agent.original": requestContext.get("userAgent")
|
||||||
|
});
|
||||||
|
}
|
||||||
logger.error(err);
|
logger.error(err);
|
||||||
done(err as Error, false);
|
done(err as Error, false);
|
||||||
}
|
}
|
||||||
@@ -204,26 +210,30 @@ export const registerOauthMiddlewares = (server: FastifyZodProvider) => {
|
|||||||
callbackPort
|
callbackPort
|
||||||
});
|
});
|
||||||
|
|
||||||
authAttemptCounter.add(1, {
|
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
|
||||||
"infisical.user.email": email,
|
authAttemptCounter.add(1, {
|
||||||
"infisical.user.id": user.id,
|
"infisical.user.email": email,
|
||||||
"infisical.organization.id": orgId,
|
"infisical.user.id": user.id,
|
||||||
"infisical.organization.name": orgName,
|
"infisical.organization.id": orgId,
|
||||||
"infisical.auth.method": AuthAttemptAuthMethod.GITLAB,
|
"infisical.organization.name": orgName,
|
||||||
"infisical.auth.result": AuthAttemptAuthResult.SUCCESS,
|
"infisical.auth.method": AuthAttemptAuthMethod.GITLAB,
|
||||||
"client.address": requestContext.get("ip"),
|
"infisical.auth.result": AuthAttemptAuthResult.SUCCESS,
|
||||||
"user_agent.original": requestContext.get("userAgent")
|
"client.address": requestContext.get("ip"),
|
||||||
});
|
"user_agent.original": requestContext.get("userAgent")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return cb(null, { isUserCompleted, providerAuthToken });
|
return cb(null, { isUserCompleted, providerAuthToken });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
authAttemptCounter.add(1, {
|
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
|
||||||
"infisical.user.email": email,
|
authAttemptCounter.add(1, {
|
||||||
"infisical.auth.method": AuthAttemptAuthMethod.GITLAB,
|
"infisical.user.email": email,
|
||||||
"infisical.auth.result": AuthAttemptAuthResult.FAILURE,
|
"infisical.auth.method": AuthAttemptAuthMethod.GITLAB,
|
||||||
"client.address": requestContext.get("ip"),
|
"infisical.auth.result": AuthAttemptAuthResult.FAILURE,
|
||||||
"user_agent.original": requestContext.get("userAgent")
|
"client.address": requestContext.get("ip"),
|
||||||
});
|
"user_agent.original": requestContext.get("userAgent")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
logger.error(error);
|
logger.error(error);
|
||||||
cb(error as Error, false);
|
cb(error as Error, false);
|
||||||
|
|||||||
@@ -386,6 +386,8 @@ export const authLoginServiceFactory = ({
|
|||||||
providerAuthToken?: string;
|
providerAuthToken?: string;
|
||||||
captchaToken?: string;
|
captchaToken?: string;
|
||||||
}) => {
|
}) => {
|
||||||
|
const appCfg = getConfig();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const usersByUsername = await userDAL.findUserEncKeyByUsername({
|
const usersByUsername = await userDAL.findUserEncKeyByUsername({
|
||||||
username: email
|
username: email
|
||||||
@@ -440,15 +442,17 @@ export const authLoginServiceFactory = ({
|
|||||||
organizationId
|
organizationId
|
||||||
});
|
});
|
||||||
|
|
||||||
authAttemptCounter.add(1, {
|
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
|
||||||
"infisical.organization.id": organizationId,
|
authAttemptCounter.add(1, {
|
||||||
"infisical.user.email": email,
|
"infisical.organization.id": organizationId,
|
||||||
"infisical.user.id": userEnc.userId,
|
"infisical.user.email": email,
|
||||||
"infisical.auth.method": AuthAttemptAuthMethod.EMAIL,
|
"infisical.user.id": userEnc.userId,
|
||||||
"infisical.auth.result": AuthAttemptAuthResult.SUCCESS,
|
"infisical.auth.method": AuthAttemptAuthMethod.EMAIL,
|
||||||
"client.address": ip,
|
"infisical.auth.result": AuthAttemptAuthResult.SUCCESS,
|
||||||
"user_agent.original": userAgent
|
"client.address": ip,
|
||||||
});
|
"user_agent.original": userAgent
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
tokens: {
|
tokens: {
|
||||||
@@ -458,13 +462,15 @@ export const authLoginServiceFactory = ({
|
|||||||
user: userEnc
|
user: userEnc
|
||||||
} as const;
|
} as const;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
authAttemptCounter.add(1, {
|
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
|
||||||
"infisical.user.email": email,
|
authAttemptCounter.add(1, {
|
||||||
"infisical.auth.method": AuthAttemptAuthMethod.EMAIL,
|
"infisical.user.email": email,
|
||||||
"infisical.auth.result": AuthAttemptAuthResult.FAILURE,
|
"infisical.auth.method": AuthAttemptAuthMethod.EMAIL,
|
||||||
"client.address": ip,
|
"infisical.auth.result": AuthAttemptAuthResult.FAILURE,
|
||||||
"user_agent.original": userAgent
|
"client.address": ip,
|
||||||
});
|
"user_agent.original": userAgent
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
import { requestContext } from "@fastify/request-context";
|
||||||
import { AxiosError } from "axios";
|
import { AxiosError } from "axios";
|
||||||
|
|
||||||
import { AccessScope, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas";
|
import { AccessScope, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas";
|
||||||
@@ -22,6 +23,7 @@ import {
|
|||||||
} from "@app/lib/errors";
|
} from "@app/lib/errors";
|
||||||
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
|
import { AuthAttemptAuthMethod, AuthAttemptAuthResult, authAttemptCounter } from "@app/lib/telemetry/metrics";
|
||||||
|
|
||||||
import { ActorType, AuthTokenType } from "../auth/auth-type";
|
import { ActorType, AuthTokenType } from "../auth/auth-type";
|
||||||
import { TIdentityDALFactory } from "../identity/identity-dal";
|
import { TIdentityDALFactory } from "../identity/identity-dal";
|
||||||
@@ -65,6 +67,7 @@ export const identityAliCloudAuthServiceFactory = ({
|
|||||||
orgDAL
|
orgDAL
|
||||||
}: TIdentityAliCloudAuthServiceFactoryDep) => {
|
}: TIdentityAliCloudAuthServiceFactoryDep) => {
|
||||||
const login = async ({ identityId, ...params }: TLoginAliCloudAuthDTO) => {
|
const login = async ({ identityId, ...params }: TLoginAliCloudAuthDTO) => {
|
||||||
|
const appCfg = getConfig();
|
||||||
const identityAliCloudAuth = await identityAliCloudAuthDAL.findOne({ identityId });
|
const identityAliCloudAuth = await identityAliCloudAuthDAL.findOne({ identityId });
|
||||||
if (!identityAliCloudAuth) {
|
if (!identityAliCloudAuth) {
|
||||||
throw new NotFoundError({
|
throw new NotFoundError({
|
||||||
@@ -75,73 +78,103 @@ export const identityAliCloudAuthServiceFactory = ({
|
|||||||
const identity = await identityDAL.findById(identityAliCloudAuth.identityId);
|
const identity = await identityDAL.findById(identityAliCloudAuth.identityId);
|
||||||
if (!identity) throw new UnauthorizedError({ message: "Identity not found" });
|
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)) {
|
try {
|
||||||
requestUrl.searchParams.set(key, (params as Record<string, string>)[key]);
|
const requestUrl = new URL("https://sts.aliyuncs.com");
|
||||||
}
|
|
||||||
|
|
||||||
const { data } = await request.get<TAliCloudGetUserResponse>(requestUrl.toString()).catch((err: AxiosError) => {
|
for (const key of Object.keys(params)) {
|
||||||
logger.error(err.response, "AliCloudIdentityLogin: Failed to authenticate with Alibaba Cloud");
|
requestUrl.searchParams.set(key, (params as Record<string, string>)[key]);
|
||||||
throw err;
|
}
|
||||||
});
|
|
||||||
|
|
||||||
if (identityAliCloudAuth.allowedArns) {
|
const { data } = await request.get<TAliCloudGetUserResponse>(requestUrl.toString()).catch((err: AxiosError) => {
|
||||||
// In the future we could do partial checks for role ARNs
|
logger.error(err.response, "AliCloudIdentityLogin: Failed to authenticate with Alibaba Cloud");
|
||||||
const isAccountAllowed = identityAliCloudAuth.allowedArns.split(",").some((arn) => arn.trim() === data.Arn);
|
throw err;
|
||||||
|
});
|
||||||
|
|
||||||
if (!isAccountAllowed)
|
if (identityAliCloudAuth.allowedArns) {
|
||||||
throw new UnauthorizedError({
|
// In the future we could do partial checks for role ARNs
|
||||||
message: "Access denied: Alibaba Cloud account ARN not allowed."
|
const isAccountAllowed = identityAliCloudAuth.allowedArns.split(",").some((arn) => arn.trim() === data.Arn);
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate the token
|
if (!isAccountAllowed)
|
||||||
const identityAccessToken = await identityAliCloudAuthDAL.transaction(async (tx) => {
|
throw new UnauthorizedError({
|
||||||
await membershipIdentityDAL.update(
|
message: "Access denied: Alibaba Cloud account ARN not allowed."
|
||||||
{ scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id },
|
});
|
||||||
{
|
}
|
||||||
lastLoginAuthMethod: IdentityAuthMethod.ALICLOUD_AUTH,
|
|
||||||
lastLoginTime: new Date()
|
// Generate the token
|
||||||
},
|
const identityAccessToken = await identityAliCloudAuthDAL.transaction(async (tx) => {
|
||||||
tx
|
await membershipIdentityDAL.update(
|
||||||
);
|
{ scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id },
|
||||||
const newToken = await identityAccessTokenDAL.create(
|
{
|
||||||
|
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,
|
identityId: identityAliCloudAuth.identityId,
|
||||||
isAccessTokenRevoked: false,
|
identityAccessTokenId: identityAccessToken.id,
|
||||||
accessTokenTTL: identityAliCloudAuth.accessTokenTTL,
|
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
|
||||||
accessTokenMaxTTL: identityAliCloudAuth.accessTokenMaxTTL,
|
} as TIdentityAccessTokenJwtPayload,
|
||||||
accessTokenNumUses: 0,
|
appCfg.AUTH_SECRET,
|
||||||
accessTokenNumUsesLimit: identityAliCloudAuth.accessTokenNumUsesLimit,
|
Number(identityAccessToken.accessTokenTTL) === 0
|
||||||
authMethod: IdentityAuthMethod.ALICLOUD_AUTH
|
? undefined
|
||||||
},
|
: {
|
||||||
tx
|
expiresIn: Number(identityAccessToken.accessTokenTTL)
|
||||||
|
}
|
||||||
);
|
);
|
||||||
return newToken;
|
|
||||||
});
|
|
||||||
|
|
||||||
const appCfg = getConfig();
|
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
|
||||||
const accessToken = crypto.jwt().sign(
|
authAttemptCounter.add(1, {
|
||||||
{
|
"infisical.identity.id": identityAliCloudAuth.identityId,
|
||||||
identityId: identityAliCloudAuth.identityId,
|
"infisical.identity.name": identity.name,
|
||||||
identityAccessTokenId: identityAccessToken.id,
|
"infisical.organization.id": org.id,
|
||||||
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
|
"infisical.organization.name": org.name,
|
||||||
} as TIdentityAccessTokenJwtPayload,
|
"infisical.identity.auth_method": AuthAttemptAuthMethod.ALICLOUD_AUTH,
|
||||||
appCfg.AUTH_SECRET,
|
"infisical.identity.auth_result": AuthAttemptAuthResult.SUCCESS,
|
||||||
Number(identityAccessToken.accessTokenTTL) === 0
|
"client.address": requestContext.get("ip"),
|
||||||
? undefined
|
"user_agent.original": requestContext.get("userAgent")
|
||||||
: {
|
});
|
||||||
expiresIn: Number(identityAccessToken.accessTokenTTL)
|
}
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
identityAliCloudAuth,
|
identityAliCloudAuth,
|
||||||
accessToken,
|
accessToken,
|
||||||
identityAccessToken,
|
identityAccessToken,
|
||||||
identity
|
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 ({
|
const attachAliCloudAuth = async ({
|
||||||
|
|||||||
@@ -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 { ForbiddenError } from "@casl/ability";
|
||||||
|
import { requestContext } from "@fastify/request-context";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import RE2 from "re2";
|
import RE2 from "re2";
|
||||||
|
|
||||||
@@ -22,6 +23,7 @@ import {
|
|||||||
} from "@app/lib/errors";
|
} from "@app/lib/errors";
|
||||||
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
|
import { AuthAttemptAuthMethod, AuthAttemptAuthResult, authAttemptCounter } from "@app/lib/telemetry/metrics";
|
||||||
|
|
||||||
import { ActorType, AuthTokenType } from "../auth/auth-type";
|
import { ActorType, AuthTokenType } from "../auth/auth-type";
|
||||||
import { TIdentityDALFactory } from "../identity/identity-dal";
|
import { TIdentityDALFactory } from "../identity/identity-dal";
|
||||||
@@ -98,6 +100,7 @@ export const identityAwsAuthServiceFactory = ({
|
|||||||
orgDAL
|
orgDAL
|
||||||
}: TIdentityAwsAuthServiceFactoryDep) => {
|
}: TIdentityAwsAuthServiceFactoryDep) => {
|
||||||
const login = async ({ identityId, iamHttpRequestMethod, iamRequestBody, iamRequestHeaders }: TLoginAwsAuthDTO) => {
|
const login = async ({ identityId, iamHttpRequestMethod, iamRequestBody, iamRequestHeaders }: TLoginAwsAuthDTO) => {
|
||||||
|
const appCfg = getConfig();
|
||||||
const identityAwsAuth = await identityAwsAuthDAL.findOne({ identityId });
|
const identityAwsAuth = await identityAwsAuthDAL.findOne({ identityId });
|
||||||
if (!identityAwsAuth) {
|
if (!identityAwsAuth) {
|
||||||
throw new NotFoundError({ message: "AWS auth method not found for identity, did you configure AWS auth?" });
|
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);
|
const identity = await identityDAL.findById(identityAwsAuth.identityId);
|
||||||
if (!identity) throw new UnauthorizedError({ message: "Identity not found" });
|
if (!identity) throw new UnauthorizedError({ message: "Identity not found" });
|
||||||
|
|
||||||
const headers: TAwsGetCallerIdentityHeaders = JSON.parse(Buffer.from(iamRequestHeaders, "base64").toString());
|
const org = await orgDAL.findById(identity.orgId);
|
||||||
const body: string = Buffer.from(iamRequestBody, "base64").toString();
|
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 authHeader = headers.Authorization || headers.authorization;
|
||||||
const region = authHeader ? awsRegionFromHeader(authHeader) : null;
|
const region = authHeader ? awsRegionFromHeader(authHeader) : null;
|
||||||
|
|
||||||
if (!isValidAwsRegion(region)) {
|
if (!isValidAwsRegion(region)) {
|
||||||
throw new BadRequestError({ message: "Invalid AWS 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 {
|
const {
|
||||||
data: {
|
data: {
|
||||||
GetCallerIdentityResponse: {
|
GetCallerIdentityResponse: {
|
||||||
GetCallerIdentityResult: { Account, Arn, UserId }
|
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) {
|
const identityAccessToken = await identityAwsAuthDAL.transaction(async (tx) => {
|
||||||
// validate if Account is in the list of allowed Account IDs
|
await membershipIdentityDAL.update(
|
||||||
|
{ scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id },
|
||||||
const isAccountAllowed = identityAwsAuth.allowedAccountIds
|
{
|
||||||
.split(",")
|
lastLoginAuthMethod: IdentityAuthMethod.AWS_AUTH,
|
||||||
.map((accountId) => accountId.trim())
|
lastLoginTime: new Date()
|
||||||
.some((accountId) => accountId === Account);
|
},
|
||||||
|
tx
|
||||||
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 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({
|
const splitArn = extractPrincipalArnEntity(Arn);
|
||||||
message: `Access denied: AWS principal ARN not allowed. [principal-arn=${formattedArn}]`
|
const accessToken = crypto.jwt().sign(
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
identityId: identityAwsAuth.identityId,
|
||||||
isAccessTokenRevoked: false,
|
identityAccessTokenId: identityAccessToken.id,
|
||||||
accessTokenTTL: identityAwsAuth.accessTokenTTL,
|
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN,
|
||||||
accessTokenMaxTTL: identityAwsAuth.accessTokenMaxTTL,
|
identityAuth: {
|
||||||
accessTokenNumUses: 0,
|
aws: {
|
||||||
accessTokenNumUsesLimit: identityAwsAuth.accessTokenNumUsesLimit,
|
accountId: Account,
|
||||||
authMethod: IdentityAuthMethod.AWS_AUTH
|
arn: Arn,
|
||||||
},
|
userId: UserId,
|
||||||
tx
|
|
||||||
|
// 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();
|
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
|
||||||
const splitArn = extractPrincipalArnEntity(Arn);
|
authAttemptCounter.add(1, {
|
||||||
const accessToken = crypto.jwt().sign(
|
"infisical.identity.id": identityAwsAuth.identityId,
|
||||||
{
|
"infisical.identity.name": identity.name,
|
||||||
identityId: identityAwsAuth.identityId,
|
"infisical.organization.id": org.id,
|
||||||
identityAccessTokenId: identityAccessToken.id,
|
"infisical.organization.name": org.name,
|
||||||
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN,
|
"infisical.identity.auth_method": AuthAttemptAuthMethod.AWS_AUTH,
|
||||||
identityAuth: {
|
"infisical.identity.auth_result": AuthAttemptAuthResult.SUCCESS,
|
||||||
aws: {
|
"client.address": requestContext.get("ip"),
|
||||||
accountId: Account,
|
"user_agent.original": requestContext.get("userAgent")
|
||||||
arn: Arn,
|
});
|
||||||
userId: UserId,
|
}
|
||||||
|
|
||||||
// Derived from ARN
|
return { accessToken, identityAwsAuth, identityAccessToken, identity };
|
||||||
partition: splitArn.Partition,
|
} catch (error) {
|
||||||
service: splitArn.Service,
|
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
|
||||||
resourceType: splitArn.Type,
|
authAttemptCounter.add(1, {
|
||||||
resourceName: splitArn.FriendlyName
|
"infisical.identity.id": identityAwsAuth.identityId,
|
||||||
}
|
"infisical.identity.name": identity.name,
|
||||||
}
|
"infisical.organization.id": org.id,
|
||||||
} as TIdentityAccessTokenJwtPayload,
|
"infisical.organization.name": org.name,
|
||||||
appCfg.AUTH_SECRET,
|
"infisical.identity.auth_method": AuthAttemptAuthMethod.AWS_AUTH,
|
||||||
// akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error
|
"infisical.identity.auth_result": AuthAttemptAuthResult.FAILURE,
|
||||||
Number(identityAccessToken.accessTokenTTL) === 0
|
"client.address": requestContext.get("ip"),
|
||||||
? undefined
|
"user_agent.original": requestContext.get("userAgent")
|
||||||
: {
|
});
|
||||||
expiresIn: Number(identityAccessToken.accessTokenTTL)
|
}
|
||||||
}
|
throw error;
|
||||||
);
|
}
|
||||||
|
|
||||||
return { accessToken, identityAwsAuth, identityAccessToken, identity };
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const attachAwsAuth = async ({
|
const attachAwsAuth = async ({
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
import { requestContext } from "@fastify/request-context";
|
||||||
|
|
||||||
import { AccessScope, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas";
|
import { AccessScope, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas";
|
||||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
@@ -18,6 +19,7 @@ import {
|
|||||||
UnauthorizedError
|
UnauthorizedError
|
||||||
} from "@app/lib/errors";
|
} from "@app/lib/errors";
|
||||||
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
||||||
|
import { AuthAttemptAuthMethod, AuthAttemptAuthResult, authAttemptCounter } from "@app/lib/telemetry/metrics";
|
||||||
|
|
||||||
import { ActorType, AuthTokenType } from "../auth/auth-type";
|
import { ActorType, AuthTokenType } from "../auth/auth-type";
|
||||||
import { TIdentityDALFactory } from "../identity/identity-dal";
|
import { TIdentityDALFactory } from "../identity/identity-dal";
|
||||||
@@ -61,6 +63,7 @@ export const identityAzureAuthServiceFactory = ({
|
|||||||
orgDAL
|
orgDAL
|
||||||
}: TIdentityAzureAuthServiceFactoryDep) => {
|
}: TIdentityAzureAuthServiceFactoryDep) => {
|
||||||
const login = async ({ identityId, jwt: azureJwt }: TLoginAzureAuthDTO) => {
|
const login = async ({ identityId, jwt: azureJwt }: TLoginAzureAuthDTO) => {
|
||||||
|
const appCfg = getConfig();
|
||||||
const identityAzureAuth = await identityAzureAuthDAL.findOne({ identityId });
|
const identityAzureAuth = await identityAzureAuthDAL.findOne({ identityId });
|
||||||
if (!identityAzureAuth) {
|
if (!identityAzureAuth) {
|
||||||
throw new NotFoundError({ message: "Azure auth method not found for identity, did you configure Azure Auth?" });
|
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);
|
const identity = await identityDAL.findById(identityAzureAuth.identityId);
|
||||||
if (!identity) throw new UnauthorizedError({ message: "Identity not found" });
|
if (!identity) throw new UnauthorizedError({ message: "Identity not found" });
|
||||||
|
|
||||||
const azureIdentity = await validateAzureIdentity({
|
const org = await orgDAL.findById(identity.orgId);
|
||||||
tenantId: identityAzureAuth.tenantId,
|
|
||||||
resource: identityAzureAuth.resource,
|
|
||||||
jwt: azureJwt
|
|
||||||
});
|
|
||||||
|
|
||||||
if (azureIdentity.tid !== identityAzureAuth.tenantId)
|
try {
|
||||||
throw new UnauthorizedError({ message: "Tenant ID mismatch" });
|
const azureIdentity = await validateAzureIdentity({
|
||||||
|
tenantId: identityAzureAuth.tenantId,
|
||||||
|
resource: identityAzureAuth.resource,
|
||||||
|
jwt: azureJwt
|
||||||
|
});
|
||||||
|
|
||||||
if (identityAzureAuth.allowedServicePrincipalIds) {
|
if (azureIdentity.tid !== identityAzureAuth.tenantId)
|
||||||
// validate if the service principal id is in the list of allowed service principal ids
|
throw new UnauthorizedError({ message: "Tenant ID mismatch" });
|
||||||
|
|
||||||
const isServicePrincipalAllowed = identityAzureAuth.allowedServicePrincipalIds
|
if (identityAzureAuth.allowedServicePrincipalIds) {
|
||||||
.split(",")
|
// validate if the service principal id is in the list of allowed service principal ids
|
||||||
.map((servicePrincipalId) => servicePrincipalId.trim())
|
|
||||||
.some((servicePrincipalId) => servicePrincipalId === azureIdentity.oid);
|
|
||||||
|
|
||||||
if (!isServicePrincipalAllowed) {
|
const isServicePrincipalAllowed = identityAzureAuth.allowedServicePrincipalIds
|
||||||
throw new UnauthorizedError({ message: `Service principal '${azureIdentity.oid}' not allowed` });
|
.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) => {
|
const identityAccessToken = await identityAzureAuthDAL.transaction(async (tx) => {
|
||||||
await membershipIdentityDAL.update(
|
await membershipIdentityDAL.update(
|
||||||
{ scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id },
|
{ scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id },
|
||||||
{
|
{
|
||||||
lastLoginAuthMethod: IdentityAuthMethod.AZURE_AUTH,
|
lastLoginAuthMethod: IdentityAuthMethod.AZURE_AUTH,
|
||||||
lastLoginTime: new Date()
|
lastLoginTime: new Date()
|
||||||
},
|
},
|
||||||
tx
|
tx
|
||||||
);
|
);
|
||||||
const newToken = await identityAccessTokenDAL.create(
|
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,
|
identityId: identityAzureAuth.identityId,
|
||||||
isAccessTokenRevoked: false,
|
identityAccessTokenId: identityAccessToken.id,
|
||||||
accessTokenTTL: identityAzureAuth.accessTokenTTL,
|
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
|
||||||
accessTokenMaxTTL: identityAzureAuth.accessTokenMaxTTL,
|
} as TIdentityAccessTokenJwtPayload,
|
||||||
accessTokenNumUses: 0,
|
appCfg.AUTH_SECRET,
|
||||||
accessTokenNumUsesLimit: identityAzureAuth.accessTokenNumUsesLimit,
|
// akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error
|
||||||
authMethod: IdentityAuthMethod.AZURE_AUTH
|
Number(identityAccessToken.accessTokenTTL) === 0
|
||||||
},
|
? undefined
|
||||||
tx
|
: {
|
||||||
|
expiresIn: Number(identityAccessToken.accessTokenTTL)
|
||||||
|
}
|
||||||
);
|
);
|
||||||
return newToken;
|
|
||||||
});
|
|
||||||
|
|
||||||
const appCfg = getConfig();
|
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
|
||||||
const accessToken = crypto.jwt().sign(
|
authAttemptCounter.add(1, {
|
||||||
{
|
"infisical.identity.id": identityAzureAuth.identityId,
|
||||||
identityId: identityAzureAuth.identityId,
|
"infisical.identity.name": identity.name,
|
||||||
identityAccessTokenId: identityAccessToken.id,
|
"infisical.organization.id": org.id,
|
||||||
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
|
"infisical.organization.name": org.name,
|
||||||
} as TIdentityAccessTokenJwtPayload,
|
"infisical.identity.auth_method": AuthAttemptAuthMethod.AZURE_AUTH,
|
||||||
appCfg.AUTH_SECRET,
|
"infisical.identity.auth_result": AuthAttemptAuthResult.SUCCESS,
|
||||||
// akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error
|
"client.address": requestContext.get("ip"),
|
||||||
Number(identityAccessToken.accessTokenTTL) === 0
|
"user_agent.original": requestContext.get("userAgent")
|
||||||
? undefined
|
});
|
||||||
: {
|
}
|
||||||
expiresIn: Number(identityAccessToken.accessTokenTTL)
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
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 ({
|
const attachAzureAuth = async ({
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
import { requestContext } from "@fastify/request-context";
|
||||||
|
|
||||||
import { AccessScope, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas";
|
import { AccessScope, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas";
|
||||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
@@ -18,6 +19,7 @@ import {
|
|||||||
UnauthorizedError
|
UnauthorizedError
|
||||||
} from "@app/lib/errors";
|
} from "@app/lib/errors";
|
||||||
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
||||||
|
import { AuthAttemptAuthMethod, AuthAttemptAuthResult, authAttemptCounter } from "@app/lib/telemetry/metrics";
|
||||||
|
|
||||||
import { ActorType, AuthTokenType } from "../auth/auth-type";
|
import { ActorType, AuthTokenType } from "../auth/auth-type";
|
||||||
import { TIdentityDALFactory } from "../identity/identity-dal";
|
import { TIdentityDALFactory } from "../identity/identity-dal";
|
||||||
@@ -59,6 +61,7 @@ export const identityGcpAuthServiceFactory = ({
|
|||||||
orgDAL
|
orgDAL
|
||||||
}: TIdentityGcpAuthServiceFactoryDep) => {
|
}: TIdentityGcpAuthServiceFactoryDep) => {
|
||||||
const login = async ({ identityId, jwt: gcpJwt }: TLoginGcpAuthDTO) => {
|
const login = async ({ identityId, jwt: gcpJwt }: TLoginGcpAuthDTO) => {
|
||||||
|
const appCfg = getConfig();
|
||||||
const identityGcpAuth = await identityGcpAuthDAL.findOne({ identityId });
|
const identityGcpAuth = await identityGcpAuthDAL.findOne({ identityId });
|
||||||
if (!identityGcpAuth) {
|
if (!identityGcpAuth) {
|
||||||
throw new NotFoundError({ message: "GCP auth method not found for identity, did you configure GCP auth?" });
|
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);
|
const identity = await identityDAL.findById(identityGcpAuth.identityId);
|
||||||
if (!identity) throw new UnauthorizedError({ message: "Identity not found" });
|
if (!identity) throw new UnauthorizedError({ message: "Identity not found" });
|
||||||
|
|
||||||
let gcpIdentityDetails: TGcpIdentityDetails;
|
const org = await orgDAL.findById(identity.orgId);
|
||||||
switch (identityGcpAuth.type) {
|
try {
|
||||||
case "gce": {
|
let gcpIdentityDetails: TGcpIdentityDetails;
|
||||||
gcpIdentityDetails = await validateIdTokenIdentity({
|
switch (identityGcpAuth.type) {
|
||||||
identityId,
|
case "gce": {
|
||||||
jwt: gcpJwt
|
gcpIdentityDetails = await validateIdTokenIdentity({
|
||||||
});
|
identityId,
|
||||||
break;
|
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({
|
if (identityGcpAuth.allowedServiceAccounts) {
|
||||||
identityId,
|
// validate if the service account is in the list of allowed service accounts
|
||||||
jwt: gcpJwt
|
|
||||||
});
|
const isServiceAccountAllowed = identityGcpAuth.allowedServiceAccounts
|
||||||
break;
|
.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) {
|
if (identityGcpAuth.type === "gce" && identityGcpAuth.allowedZones && gcpIdentityDetails.computeEngineDetails) {
|
||||||
// validate if the service account is in the list of allowed service accounts
|
const isZoneAllowed = identityGcpAuth.allowedZones
|
||||||
|
.split(",")
|
||||||
|
.map((zone) => zone.trim())
|
||||||
|
.some((zone) => zone === gcpIdentityDetails.computeEngineDetails?.zone);
|
||||||
|
|
||||||
const isServiceAccountAllowed = identityGcpAuth.allowedServiceAccounts
|
if (!isZoneAllowed)
|
||||||
.split(",")
|
throw new UnauthorizedError({
|
||||||
.map((serviceAccount) => serviceAccount.trim())
|
message: "Access denied: GCP zone not allowed."
|
||||||
.some((serviceAccount) => serviceAccount === gcpIdentityDetails.email);
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (!isServiceAccountAllowed)
|
const identityAccessToken = await identityGcpAuthDAL.transaction(async (tx) => {
|
||||||
throw new UnauthorizedError({
|
await membershipIdentityDAL.update(
|
||||||
message: "Access denied: GCP service account not allowed."
|
{ scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id },
|
||||||
});
|
{
|
||||||
}
|
lastLoginAuthMethod: IdentityAuthMethod.GCP_AUTH,
|
||||||
|
lastLoginTime: new Date()
|
||||||
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
|
tx
|
||||||
|
);
|
||||||
const isProjectAllowed = identityGcpAuth.allowedProjects
|
const newToken = await identityAccessTokenDAL.create(
|
||||||
.split(",")
|
{
|
||||||
.map((project) => project.trim())
|
identityId: identityGcpAuth.identityId,
|
||||||
.some((project) => project === gcpIdentityDetails.computeEngineDetails?.project_id);
|
isAccessTokenRevoked: false,
|
||||||
|
accessTokenTTL: identityGcpAuth.accessTokenTTL,
|
||||||
if (!isProjectAllowed)
|
accessTokenMaxTTL: identityGcpAuth.accessTokenMaxTTL,
|
||||||
throw new UnauthorizedError({
|
accessTokenNumUses: 0,
|
||||||
message: "Access denied: GCP project not allowed."
|
accessTokenNumUsesLimit: identityGcpAuth.accessTokenNumUsesLimit,
|
||||||
});
|
authMethod: IdentityAuthMethod.GCP_AUTH
|
||||||
}
|
},
|
||||||
|
tx
|
||||||
if (identityGcpAuth.type === "gce" && identityGcpAuth.allowedZones && gcpIdentityDetails.computeEngineDetails) {
|
);
|
||||||
const isZoneAllowed = identityGcpAuth.allowedZones
|
return newToken;
|
||||||
.split(",")
|
});
|
||||||
.map((zone) => zone.trim())
|
const accessToken = crypto.jwt().sign(
|
||||||
.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(
|
|
||||||
{
|
{
|
||||||
identityId: identityGcpAuth.identityId,
|
identityId: identityGcpAuth.identityId,
|
||||||
isAccessTokenRevoked: false,
|
identityAccessTokenId: identityAccessToken.id,
|
||||||
accessTokenTTL: identityGcpAuth.accessTokenTTL,
|
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
|
||||||
accessTokenMaxTTL: identityGcpAuth.accessTokenMaxTTL,
|
} as TIdentityAccessTokenJwtPayload,
|
||||||
accessTokenNumUses: 0,
|
appCfg.AUTH_SECRET,
|
||||||
accessTokenNumUsesLimit: identityGcpAuth.accessTokenNumUsesLimit,
|
// akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error
|
||||||
authMethod: IdentityAuthMethod.GCP_AUTH
|
Number(identityAccessToken.accessTokenTTL) === 0
|
||||||
},
|
? undefined
|
||||||
tx
|
: {
|
||||||
|
expiresIn: Number(identityAccessToken.accessTokenTTL)
|
||||||
|
}
|
||||||
);
|
);
|
||||||
return newToken;
|
|
||||||
});
|
|
||||||
|
|
||||||
const appCfg = getConfig();
|
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
|
||||||
const accessToken = crypto.jwt().sign(
|
authAttemptCounter.add(1, {
|
||||||
{
|
"infisical.identity.id": identityGcpAuth.identityId,
|
||||||
identityId: identityGcpAuth.identityId,
|
"infisical.identity.name": identity.name,
|
||||||
identityAccessTokenId: identityAccessToken.id,
|
"infisical.organization.id": org.id,
|
||||||
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
|
"infisical.organization.name": org.name,
|
||||||
} as TIdentityAccessTokenJwtPayload,
|
"infisical.identity.auth_method": AuthAttemptAuthMethod.GCP_AUTH,
|
||||||
appCfg.AUTH_SECRET,
|
"infisical.identity.auth_result": AuthAttemptAuthResult.SUCCESS,
|
||||||
// akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error
|
"client.address": requestContext.get("ip"),
|
||||||
Number(identityAccessToken.accessTokenTTL) === 0
|
"user_agent.original": requestContext.get("userAgent")
|
||||||
? undefined
|
});
|
||||||
: {
|
}
|
||||||
expiresIn: Number(identityAccessToken.accessTokenTTL)
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
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 ({
|
const attachGcpAuth = async ({
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
import { requestContext } from "@fastify/request-context";
|
||||||
import https from "https";
|
import https from "https";
|
||||||
import jwt from "jsonwebtoken";
|
import jwt from "jsonwebtoken";
|
||||||
import { JwksClient } from "jwks-rsa";
|
import { JwksClient } from "jwks-rsa";
|
||||||
@@ -21,6 +22,7 @@ import {
|
|||||||
UnauthorizedError
|
UnauthorizedError
|
||||||
} from "@app/lib/errors";
|
} from "@app/lib/errors";
|
||||||
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
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 { getValueByDot } from "@app/lib/template/dot-access";
|
||||||
|
|
||||||
import { ActorType, AuthTokenType } from "../auth/auth-type";
|
import { ActorType, AuthTokenType } from "../auth/auth-type";
|
||||||
@@ -67,6 +69,7 @@ export const identityJwtAuthServiceFactory = ({
|
|||||||
orgDAL
|
orgDAL
|
||||||
}: TIdentityJwtAuthServiceFactoryDep) => {
|
}: TIdentityJwtAuthServiceFactoryDep) => {
|
||||||
const login = async ({ identityId, jwt: jwtValue }: TLoginJwtAuthDTO) => {
|
const login = async ({ identityId, jwt: jwtValue }: TLoginJwtAuthDTO) => {
|
||||||
|
const appCfg = getConfig();
|
||||||
const identityJwtAuth = await identityJwtAuthDAL.findOne({ identityId });
|
const identityJwtAuth = await identityJwtAuthDAL.findOne({ identityId });
|
||||||
if (!identityJwtAuth) {
|
if (!identityJwtAuth) {
|
||||||
throw new NotFoundError({ message: "JWT auth method not found for identity, did you configure JWT auth?" });
|
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);
|
const identity = await identityDAL.findById(identityJwtAuth.identityId);
|
||||||
if (!identity) throw new UnauthorizedError({ message: "Identity not found" });
|
if (!identity) throw new UnauthorizedError({ message: "Identity not found" });
|
||||||
|
|
||||||
const { decryptor: orgDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({
|
const org = await orgDAL.findById(identity.orgId);
|
||||||
type: KmsDataKey.Organization,
|
try {
|
||||||
orgId: identity.orgId
|
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"
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
let tokenData: Record<string, string | boolean | number> = {};
|
const decodedToken = crypto.jwt().decode(jwtValue, { complete: true });
|
||||||
|
if (!decodedToken) {
|
||||||
if (identityJwtAuth.configurationType === JwtConfigurationType.JWKS) {
|
throw new UnauthorizedError({
|
||||||
let client: JwksClient;
|
message: "Invalid JWT"
|
||||||
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 { kid } = decodedToken.header as { kid: string };
|
let tokenData: Record<string, string | boolean | number> = {};
|
||||||
const jwtSigningKey = await client.getSigningKey(kid);
|
|
||||||
|
|
||||||
try {
|
if (identityJwtAuth.configurationType === JwtConfigurationType.JWKS) {
|
||||||
tokenData = crypto.jwt().verify(jwtValue, jwtSigningKey.getPublicKey()) as Record<string, string>;
|
let client: JwksClient;
|
||||||
} catch (error) {
|
if (identityJwtAuth.jwksUrl.includes("https:")) {
|
||||||
if (error instanceof jwt.JsonWebTokenError) {
|
const decryptedJwksCaCert = orgDataKeyDecryptor({
|
||||||
throw new UnauthorizedError({
|
cipherTextBlob: identityJwtAuth.encryptedJwksCaCert
|
||||||
message: `Access denied: ${error.message}`
|
}).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;
|
const { kid } = decodedToken.header as { kid: string };
|
||||||
}
|
const jwtSigningKey = await client.getSigningKey(kid);
|
||||||
} else {
|
|
||||||
const decryptedPublicKeys = orgDataKeyDecryptor({ cipherTextBlob: identityJwtAuth.encryptedPublicKeys })
|
|
||||||
.toString()
|
|
||||||
.split(",");
|
|
||||||
|
|
||||||
const errors: string[] = [];
|
|
||||||
let isMatchAnyKey = false;
|
|
||||||
for (const publicKey of decryptedPublicKeys) {
|
|
||||||
try {
|
try {
|
||||||
tokenData = crypto.jwt().verify(jwtValue, publicKey) as Record<string, string>;
|
tokenData = crypto.jwt().verify(jwtValue, jwtSigningKey.getPublicKey()) as Record<string, string>;
|
||||||
isMatchAnyKey = true;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof jwt.JsonWebTokenError) {
|
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) {
|
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) {
|
|
||||||
throw new UnauthorizedError({
|
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 (!doesFieldValueMatchJwtPolicy(tokenData.sub, identityJwtAuth.boundSubject)) {
|
||||||
if (!claimValue.split(", ").some((claimEntry) => doesFieldValueMatchJwtPolicy(value, claimEntry))) {
|
throw new ForbiddenRequestError({
|
||||||
throw new UnauthorizedError({
|
message: "Access denied: subject not allowed"
|
||||||
message: `Access denied: claim mismatch for field ${claimKey}`
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) => {
|
const accessToken = crypto.jwt().sign(
|
||||||
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,
|
identityId: identityJwtAuth.identityId,
|
||||||
isAccessTokenRevoked: false,
|
identityAccessTokenId: identityAccessToken.id,
|
||||||
accessTokenTTL: identityJwtAuth.accessTokenTTL,
|
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
|
||||||
accessTokenMaxTTL: identityJwtAuth.accessTokenMaxTTL,
|
} as TIdentityAccessTokenJwtPayload,
|
||||||
accessTokenNumUses: 0,
|
appCfg.AUTH_SECRET,
|
||||||
accessTokenNumUsesLimit: identityJwtAuth.accessTokenNumUsesLimit,
|
// akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error
|
||||||
authMethod: IdentityAuthMethod.JWT_AUTH
|
Number(identityAccessToken.accessTokenTTL) === 0
|
||||||
},
|
? undefined
|
||||||
tx
|
: {
|
||||||
|
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();
|
return { accessToken, identityJwtAuth, identityAccessToken, identity };
|
||||||
const accessToken = crypto.jwt().sign(
|
} catch (error) {
|
||||||
{
|
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
|
||||||
identityId: identityJwtAuth.identityId,
|
authAttemptCounter.add(1, {
|
||||||
identityAccessTokenId: identityAccessToken.id,
|
"infisical.identity.id": identityJwtAuth.identityId,
|
||||||
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
|
"infisical.identity.name": identity.name,
|
||||||
} as TIdentityAccessTokenJwtPayload,
|
"infisical.organization.id": org.id,
|
||||||
appCfg.AUTH_SECRET,
|
"infisical.organization.name": org.name,
|
||||||
// akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error
|
"infisical.identity.auth_method": AuthAttemptAuthMethod.JWT_AUTH,
|
||||||
Number(identityAccessToken.accessTokenTTL) === 0
|
"infisical.identity.auth_result": AuthAttemptAuthResult.FAILURE,
|
||||||
? undefined
|
"client.address": requestContext.get("ip"),
|
||||||
: {
|
"user_agent.original": requestContext.get("userAgent")
|
||||||
expiresIn: Number(identityAccessToken.accessTokenTTL)
|
});
|
||||||
}
|
}
|
||||||
);
|
throw error;
|
||||||
|
}
|
||||||
return { accessToken, identityJwtAuth, identityAccessToken, identity };
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const attachJwtAuth = async ({
|
const attachJwtAuth = async ({
|
||||||
|
|||||||
+305
-268
@@ -1,4 +1,5 @@
|
|||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
import { requestContext } from "@fastify/request-context";
|
||||||
import axios, { AxiosError } from "axios";
|
import axios, { AxiosError } from "axios";
|
||||||
import https from "https";
|
import https from "https";
|
||||||
import RE2 from "re2";
|
import RE2 from "re2";
|
||||||
@@ -37,6 +38,7 @@ import { GatewayHttpProxyActions, GatewayProxyProtocol, withGatewayProxy } from
|
|||||||
import { withGatewayV2Proxy } from "@app/lib/gateway-v2/gateway-v2";
|
import { withGatewayV2Proxy } from "@app/lib/gateway-v2/gateway-v2";
|
||||||
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
|
import { AuthAttemptAuthMethod, AuthAttemptAuthResult, authAttemptCounter } from "@app/lib/telemetry/metrics";
|
||||||
|
|
||||||
import { ActorType, AuthTokenType } from "../auth/auth-type";
|
import { ActorType, AuthTokenType } from "../auth/auth-type";
|
||||||
import { TIdentityDALFactory } from "../identity/identity-dal";
|
import { TIdentityDALFactory } from "../identity/identity-dal";
|
||||||
@@ -182,6 +184,7 @@ export const identityKubernetesAuthServiceFactory = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const login = async ({ identityId, jwt: serviceAccountJwt }: TLoginKubernetesAuthDTO) => {
|
const login = async ({ identityId, jwt: serviceAccountJwt }: TLoginKubernetesAuthDTO) => {
|
||||||
|
const appCfg = getConfig();
|
||||||
const identityKubernetesAuth = await identityKubernetesAuthDAL.findOne({ identityId });
|
const identityKubernetesAuth = await identityKubernetesAuthDAL.findOne({ identityId });
|
||||||
if (!identityKubernetesAuth) {
|
if (!identityKubernetesAuth) {
|
||||||
throw new NotFoundError({
|
throw new NotFoundError({
|
||||||
@@ -192,294 +195,328 @@ export const identityKubernetesAuthServiceFactory = ({
|
|||||||
const identity = await identityDAL.findById(identityKubernetesAuth.identityId);
|
const identity = await identityDAL.findById(identityKubernetesAuth.identityId);
|
||||||
if (!identity) throw new UnauthorizedError({ message: "Identity not found" });
|
if (!identity) throw new UnauthorizedError({ message: "Identity not found" });
|
||||||
|
|
||||||
const { decryptor } = await kmsService.createCipherPairWithDataKey({
|
const org = await orgDAL.findById(identity.orgId);
|
||||||
type: KmsDataKey.Organization,
|
|
||||||
orgId: identity.orgId
|
|
||||||
});
|
|
||||||
|
|
||||||
let caCert = "";
|
try {
|
||||||
if (identityKubernetesAuth.encryptedKubernetesCaCertificate) {
|
const { decryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
caCert = decryptor({ cipherTextBlob: identityKubernetesAuth.encryptedKubernetesCaCertificate }).toString();
|
type: KmsDataKey.Organization,
|
||||||
}
|
orgId: identity.orgId
|
||||||
|
});
|
||||||
|
|
||||||
const tokenReviewCallbackRaw = async (host = identityKubernetesAuth.kubernetesHost, port?: number) => {
|
let caCert = "";
|
||||||
logger.info({ host, port }, "tokenReviewCallbackRaw: Processing kubernetes token review using raw API");
|
if (identityKubernetesAuth.encryptedKubernetesCaCertificate) {
|
||||||
|
caCert = decryptor({ cipherTextBlob: identityKubernetesAuth.encryptedKubernetesCaCertificate }).toString();
|
||||||
if (!host || !identityKubernetesAuth.kubernetesHost) {
|
|
||||||
throw new BadRequestError({
|
|
||||||
message: "Kubernetes host is required when token review mode is set to API"
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let tokenReviewerJwt = "";
|
const tokenReviewCallbackRaw = async (host = identityKubernetesAuth.kubernetesHost, port?: number) => {
|
||||||
if (identityKubernetesAuth.encryptedKubernetesTokenReviewerJwt) {
|
logger.info({ host, port }, "tokenReviewCallbackRaw: Processing kubernetes token review using raw API");
|
||||||
tokenReviewerJwt = decryptor({
|
|
||||||
cipherTextBlob: identityKubernetesAuth.encryptedKubernetesTokenReviewerJwt
|
|
||||||
}).toString();
|
|
||||||
} else {
|
|
||||||
// if no token reviewer is provided means the incoming token has to act as reviewer
|
|
||||||
tokenReviewerJwt = serviceAccountJwt;
|
|
||||||
}
|
|
||||||
|
|
||||||
let servername = identityKubernetesAuth.kubernetesHost;
|
if (!host || !identityKubernetesAuth.kubernetesHost) {
|
||||||
if (servername.startsWith("https://") || servername.startsWith("http://")) {
|
throw new BadRequestError({
|
||||||
servername = new RE2("^https?:\\/\\/").replace(servername, "");
|
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
|
let tokenReviewerJwt = "";
|
||||||
const lastColonIndex = servername.lastIndexOf(":");
|
if (identityKubernetesAuth.encryptedKubernetesTokenReviewerJwt) {
|
||||||
if (lastColonIndex !== -1) {
|
tokenReviewerJwt = decryptor({
|
||||||
servername = servername.substring(0, lastColonIndex);
|
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
|
// get the last colon index, if it has a port, remove it, including the colon
|
||||||
.post<TCreateTokenReviewResponse>(
|
const lastColonIndex = servername.lastIndexOf(":");
|
||||||
`${baseUrl}/apis/authentication.k8s.io/v1/tokenreviews`,
|
if (lastColonIndex !== -1) {
|
||||||
{
|
servername = servername.substring(0, lastColonIndex);
|
||||||
apiVersion: "authentication.k8s.io/v1",
|
}
|
||||||
kind: "TokenReview",
|
|
||||||
spec: {
|
const baseUrl = port ? `${host}:${port}` : host;
|
||||||
token: serviceAccountJwt,
|
|
||||||
...(identityKubernetesAuth.allowedAudience ? { audiences: [identityKubernetesAuth.allowedAudience] } : {})
|
const res = await axios
|
||||||
}
|
.post<TCreateTokenReviewResponse>(
|
||||||
},
|
`${baseUrl}/apis/authentication.k8s.io/v1/tokenreviews`,
|
||||||
{
|
{
|
||||||
headers: {
|
apiVersion: "authentication.k8s.io/v1",
|
||||||
"Content-Type": "application/json",
|
kind: "TokenReview",
|
||||||
Authorization: `Bearer ${tokenReviewerJwt}`
|
spec: {
|
||||||
|
token: serviceAccountJwt,
|
||||||
|
...(identityKubernetesAuth.allowedAudience
|
||||||
|
? { audiences: [identityKubernetesAuth.allowedAudience] }
|
||||||
|
: {})
|
||||||
|
}
|
||||||
},
|
},
|
||||||
signal: AbortSignal.timeout(10000),
|
{
|
||||||
timeout: 10000,
|
headers: {
|
||||||
httpsAgent: new https.Agent({
|
"Content-Type": "application/json",
|
||||||
ca: caCert,
|
Authorization: `Bearer ${tokenReviewerJwt}`
|
||||||
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
|
|
||||||
},
|
},
|
||||||
tokenReviewCallbackRaw
|
signal: AbortSignal.timeout(10000),
|
||||||
)
|
timeout: 10000,
|
||||||
: await tokenReviewCallbackRaw();
|
httpsAgent: new https.Agent({
|
||||||
} else {
|
ca: caCert,
|
||||||
throw new BadRequestError({
|
rejectUnauthorized: Boolean(caCert),
|
||||||
message: `Invalid token review mode: ${identityKubernetesAuth.tokenReviewMode}`
|
servername
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
.catch((err) => {
|
||||||
|
if (err instanceof AxiosError) {
|
||||||
|
if (err.response) {
|
||||||
|
const { message } = err?.response?.data as unknown as { message?: string };
|
||||||
|
|
||||||
if (!data) {
|
if (message) {
|
||||||
throw new BadRequestError({
|
throw new UnauthorizedError({
|
||||||
message: "Failed to review token"
|
message,
|
||||||
});
|
name: "KubernetesTokenReviewRequestError"
|
||||||
}
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
|
||||||
if ("error" in data.status)
|
return res.data;
|
||||||
throw new UnauthorizedError({ message: data.status.error, name: "KubernetesTokenReviewError" });
|
};
|
||||||
|
|
||||||
// check the response to determine if the token is valid
|
const tokenReviewCallbackThroughGateway = async (host: string, port?: number) => {
|
||||||
if (!(data.status && data.status.authenticated))
|
logger.info(
|
||||||
throw new UnauthorizedError({
|
{
|
||||||
message: "Kubernetes token not authenticated",
|
host,
|
||||||
name: "KubernetesTokenReviewError"
|
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);
|
const accessToken = crypto.jwt().sign(
|
||||||
|
|
||||||
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,
|
identityId: identityKubernetesAuth.identityId,
|
||||||
isAccessTokenRevoked: false,
|
identityAccessTokenId: identityAccessToken.id,
|
||||||
accessTokenTTL: identityKubernetesAuth.accessTokenTTL,
|
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN,
|
||||||
accessTokenMaxTTL: identityKubernetesAuth.accessTokenMaxTTL,
|
identityAuth: {
|
||||||
accessTokenNumUses: 0,
|
kubernetes: {
|
||||||
accessTokenNumUsesLimit: identityKubernetesAuth.accessTokenNumUsesLimit,
|
namespace: targetNamespace,
|
||||||
authMethod: IdentityAuthMethod.KUBERNETES_AUTH
|
name: targetName
|
||||||
},
|
}
|
||||||
tx
|
}
|
||||||
|
} 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();
|
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
|
||||||
const accessToken = crypto.jwt().sign(
|
authAttemptCounter.add(1, {
|
||||||
{
|
"infisical.identity.id": identityKubernetesAuth.identityId,
|
||||||
identityId: identityKubernetesAuth.identityId,
|
"infisical.identity.name": identity.name,
|
||||||
identityAccessTokenId: identityAccessToken.id,
|
"infisical.organization.id": org.id,
|
||||||
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN,
|
"infisical.organization.name": org.name,
|
||||||
identityAuth: {
|
"infisical.identity.auth_method": AuthAttemptAuthMethod.KUBERNETES_AUTH,
|
||||||
kubernetes: {
|
"infisical.identity.auth_result": AuthAttemptAuthResult.SUCCESS,
|
||||||
namespace: targetNamespace,
|
"client.address": requestContext.get("ip"),
|
||||||
name: targetName
|
"user_agent.original": requestContext.get("userAgent")
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
} 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, 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 ({
|
const attachKubernetesAuth = async ({
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
import { requestContext } from "@fastify/request-context";
|
||||||
import slugify from "@sindresorhus/slugify";
|
import slugify from "@sindresorhus/slugify";
|
||||||
|
|
||||||
import { AccessScope, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas";
|
import { AccessScope, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas";
|
||||||
@@ -29,6 +30,7 @@ import {
|
|||||||
} from "@app/lib/errors";
|
} from "@app/lib/errors";
|
||||||
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
|
import { AuthAttemptAuthMethod, AuthAttemptAuthResult, authAttemptCounter } from "@app/lib/telemetry/metrics";
|
||||||
|
|
||||||
import { ActorType, AuthTokenType } from "../auth/auth-type";
|
import { ActorType, AuthTokenType } from "../auth/auth-type";
|
||||||
import { TIdentityDALFactory } from "../identity/identity-dal";
|
import { TIdentityDALFactory } from "../identity/identity-dal";
|
||||||
@@ -151,6 +153,7 @@ export const identityLdapAuthServiceFactory = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const login = async ({ identityId }: TLoginLdapAuthDTO) => {
|
const login = async ({ identityId }: TLoginLdapAuthDTO) => {
|
||||||
|
const appCfg = getConfig();
|
||||||
const identityLdapAuth = await identityLdapAuthDAL.findOne({ identityId });
|
const identityLdapAuth = await identityLdapAuthDAL.findOne({ identityId });
|
||||||
|
|
||||||
if (!identityLdapAuth) {
|
if (!identityLdapAuth) {
|
||||||
@@ -162,6 +165,7 @@ export const identityLdapAuthServiceFactory = ({
|
|||||||
const identity = await identityDAL.findById(identityLdapAuth.identityId);
|
const identity = await identityDAL.findById(identityLdapAuth.identityId);
|
||||||
if (!identity) throw new UnauthorizedError({ message: "Identity not found" });
|
if (!identity) throw new UnauthorizedError({ message: "Identity not found" });
|
||||||
|
|
||||||
|
const org = await orgDAL.findById(identity.orgId);
|
||||||
const plan = await licenseService.getPlan(identity.orgId);
|
const plan = await licenseService.getPlan(identity.orgId);
|
||||||
if (!plan.ldap) {
|
if (!plan.ldap) {
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({
|
||||||
@@ -170,44 +174,72 @@ export const identityLdapAuthServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const identityAccessToken = await identityLdapAuthDAL.transaction(async (tx) => {
|
try {
|
||||||
await membershipIdentityDAL.update(
|
const identityAccessToken = await identityLdapAuthDAL.transaction(async (tx) => {
|
||||||
{ scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id },
|
await membershipIdentityDAL.update(
|
||||||
{ lastLoginAuthMethod: IdentityAuthMethod.LDAP_AUTH, lastLoginTime: new Date() },
|
{ scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id },
|
||||||
tx
|
{ lastLoginAuthMethod: IdentityAuthMethod.LDAP_AUTH, lastLoginTime: new Date() },
|
||||||
);
|
tx
|
||||||
const newToken = await identityAccessTokenDAL.create(
|
);
|
||||||
|
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,
|
identityId: identityLdapAuth.identityId,
|
||||||
isAccessTokenRevoked: false,
|
identityAccessTokenId: identityAccessToken.id,
|
||||||
accessTokenTTL: identityLdapAuth.accessTokenTTL,
|
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
|
||||||
accessTokenMaxTTL: identityLdapAuth.accessTokenMaxTTL,
|
} as TIdentityAccessTokenJwtPayload,
|
||||||
accessTokenNumUses: 0,
|
appCfg.AUTH_SECRET,
|
||||||
accessTokenNumUsesLimit: identityLdapAuth.accessTokenNumUsesLimit,
|
// akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error
|
||||||
authMethod: IdentityAuthMethod.LDAP_AUTH
|
Number(identityAccessToken.accessTokenTTL) === 0
|
||||||
},
|
? undefined
|
||||||
tx
|
: {
|
||||||
|
expiresIn: Number(identityAccessToken.accessTokenTTL)
|
||||||
|
}
|
||||||
);
|
);
|
||||||
return newToken;
|
|
||||||
});
|
|
||||||
|
|
||||||
const appCfg = getConfig();
|
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
|
||||||
const accessToken = crypto.jwt().sign(
|
authAttemptCounter.add(1, {
|
||||||
{
|
"infisical.identity.id": identityLdapAuth.identityId,
|
||||||
identityId: identityLdapAuth.identityId,
|
"infisical.identity.name": identity.name,
|
||||||
identityAccessTokenId: identityAccessToken.id,
|
"infisical.organization.id": org.id,
|
||||||
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
|
"infisical.organization.name": org.name,
|
||||||
} as TIdentityAccessTokenJwtPayload,
|
"infisical.identity.auth_method": AuthAttemptAuthMethod.LDAP_AUTH,
|
||||||
appCfg.AUTH_SECRET,
|
"infisical.identity.auth_result": AuthAttemptAuthResult.SUCCESS,
|
||||||
// akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error
|
"client.address": requestContext.get("ip"),
|
||||||
Number(identityAccessToken.accessTokenTTL) === 0
|
"user_agent.original": requestContext.get("userAgent")
|
||||||
? undefined
|
});
|
||||||
: {
|
}
|
||||||
expiresIn: Number(identityAccessToken.accessTokenTTL)
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
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 ({
|
const attachLdapAuth = async ({
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
import { requestContext } from "@fastify/request-context";
|
||||||
import { AxiosError } from "axios";
|
import { AxiosError } from "axios";
|
||||||
import RE2 from "re2";
|
import RE2 from "re2";
|
||||||
|
|
||||||
@@ -23,6 +24,7 @@ import {
|
|||||||
} from "@app/lib/errors";
|
} from "@app/lib/errors";
|
||||||
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
|
import { AuthAttemptAuthMethod, AuthAttemptAuthResult, authAttemptCounter } from "@app/lib/telemetry/metrics";
|
||||||
|
|
||||||
import { ActorType, AuthTokenType } from "../auth/auth-type";
|
import { ActorType, AuthTokenType } from "../auth/auth-type";
|
||||||
import { TIdentityDALFactory } from "../identity/identity-dal";
|
import { TIdentityDALFactory } from "../identity/identity-dal";
|
||||||
@@ -63,6 +65,7 @@ export const identityOciAuthServiceFactory = ({
|
|||||||
orgDAL
|
orgDAL
|
||||||
}: TIdentityOciAuthServiceFactoryDep) => {
|
}: TIdentityOciAuthServiceFactoryDep) => {
|
||||||
const login = async ({ identityId, headers, userOcid }: TLoginOciAuthDTO) => {
|
const login = async ({ identityId, headers, userOcid }: TLoginOciAuthDTO) => {
|
||||||
|
const appCfg = getConfig();
|
||||||
const identityOciAuth = await identityOciAuthDAL.findOne({ identityId });
|
const identityOciAuth = await identityOciAuthDAL.findOne({ identityId });
|
||||||
if (!identityOciAuth) {
|
if (!identityOciAuth) {
|
||||||
throw new NotFoundError({ message: "OCI auth method not found for identity, did you configure OCI auth?" });
|
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);
|
const identity = await identityDAL.findById(identityOciAuth.identityId);
|
||||||
if (!identity) throw new UnauthorizedError({ message: "Identity not found" });
|
if (!identity) throw new UnauthorizedError({ message: "Identity not found" });
|
||||||
|
|
||||||
// Validate OCI host format. Ensures that the host is in "identity.<region>.oraclecloud.com" format.
|
const org = await orgDAL.findById(identity.orgId);
|
||||||
if (!headers.host || !new RE2("^identity\\.([a-z]{2}-[a-z]+-[1-9])\\.oraclecloud\\.com$").test(headers.host)) {
|
try {
|
||||||
throw new BadRequestError({
|
// Validate OCI host format. Ensures that the host is in "identity.<region>.oraclecloud.com" format.
|
||||||
message: "Invalid OCI host format. Expected format: identity.<region>.oraclecloud.com"
|
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."
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate the token
|
const { data } = await request
|
||||||
const identityAccessToken = await identityOciAuthDAL.transaction(async (tx) => {
|
.get<TOciGetUserResponse>(`https://${headers.host}/20160918/users/${userOcid}`, {
|
||||||
await membershipIdentityDAL.update(
|
headers
|
||||||
{ scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id },
|
})
|
||||||
{ lastLoginAuthMethod: IdentityAuthMethod.OCI_AUTH, lastLoginTime: new Date() },
|
.catch((err: AxiosError) => {
|
||||||
tx
|
logger.error(err.response, "OciIdentityLogin: Failed to authenticate with Oracle Cloud");
|
||||||
);
|
throw err;
|
||||||
const newToken = await identityAccessTokenDAL.create(
|
});
|
||||||
|
|
||||||
|
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,
|
identityId: identityOciAuth.identityId,
|
||||||
isAccessTokenRevoked: false,
|
identityAccessTokenId: identityAccessToken.id,
|
||||||
accessTokenTTL: identityOciAuth.accessTokenTTL,
|
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
|
||||||
accessTokenMaxTTL: identityOciAuth.accessTokenMaxTTL,
|
} as TIdentityAccessTokenJwtPayload,
|
||||||
accessTokenNumUses: 0,
|
appCfg.AUTH_SECRET,
|
||||||
accessTokenNumUsesLimit: identityOciAuth.accessTokenNumUsesLimit,
|
Number(identityAccessToken.accessTokenTTL) === 0
|
||||||
authMethod: IdentityAuthMethod.OCI_AUTH
|
? undefined
|
||||||
},
|
: {
|
||||||
tx
|
expiresIn: Number(identityAccessToken.accessTokenTTL)
|
||||||
|
}
|
||||||
);
|
);
|
||||||
return newToken;
|
|
||||||
});
|
|
||||||
|
|
||||||
const appCfg = getConfig();
|
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
|
||||||
const accessToken = crypto.jwt().sign(
|
authAttemptCounter.add(1, {
|
||||||
{
|
"infisical.identity.id": identityOciAuth.identityId,
|
||||||
identityId: identityOciAuth.identityId,
|
"infisical.identity.name": identity.name,
|
||||||
identityAccessTokenId: identityAccessToken.id,
|
"infisical.organization.id": org.id,
|
||||||
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
|
"infisical.organization.name": org.name,
|
||||||
} as TIdentityAccessTokenJwtPayload,
|
"infisical.identity.auth_method": AuthAttemptAuthMethod.OCI_AUTH,
|
||||||
appCfg.AUTH_SECRET,
|
"infisical.identity.auth_result": AuthAttemptAuthResult.SUCCESS,
|
||||||
Number(identityAccessToken.accessTokenTTL) === 0
|
"client.address": requestContext.get("ip"),
|
||||||
? undefined
|
"user_agent.original": requestContext.get("userAgent")
|
||||||
: {
|
});
|
||||||
expiresIn: Number(identityAccessToken.accessTokenTTL)
|
}
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
identityOciAuth,
|
identityOciAuth,
|
||||||
accessToken,
|
accessToken,
|
||||||
identityAccessToken,
|
identityAccessToken,
|
||||||
identity
|
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 ({
|
const attachOciAuth = async ({
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
import { requestContext } from "@fastify/request-context";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import https from "https";
|
import https from "https";
|
||||||
import jwt from "jsonwebtoken";
|
import jwt from "jsonwebtoken";
|
||||||
@@ -22,6 +23,7 @@ import {
|
|||||||
UnauthorizedError
|
UnauthorizedError
|
||||||
} from "@app/lib/errors";
|
} from "@app/lib/errors";
|
||||||
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
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 { getValueByDot } from "@app/lib/template/dot-access";
|
||||||
|
|
||||||
import { ActorType, AuthTokenType } from "../auth/auth-type";
|
import { ActorType, AuthTokenType } from "../auth/auth-type";
|
||||||
@@ -67,6 +69,7 @@ export const identityOidcAuthServiceFactory = ({
|
|||||||
orgDAL
|
orgDAL
|
||||||
}: TIdentityOidcAuthServiceFactoryDep) => {
|
}: TIdentityOidcAuthServiceFactoryDep) => {
|
||||||
const login = async ({ identityId, jwt: oidcJwt }: TLoginOidcAuthDTO) => {
|
const login = async ({ identityId, jwt: oidcJwt }: TLoginOidcAuthDTO) => {
|
||||||
|
const appCfg = getConfig();
|
||||||
const identityOidcAuth = await identityOidcAuthDAL.findOne({ identityId });
|
const identityOidcAuth = await identityOidcAuthDAL.findOne({ identityId });
|
||||||
if (!identityOidcAuth) {
|
if (!identityOidcAuth) {
|
||||||
throw new NotFoundError({ message: "OIDC auth method not found for identity, did you configure OIDC auth?" });
|
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);
|
const identity = await identityDAL.findById(identityOidcAuth.identityId);
|
||||||
if (!identity) throw new UnauthorizedError({ message: "Identity not found" });
|
if (!identity) throw new UnauthorizedError({ message: "Identity not found" });
|
||||||
|
|
||||||
const { decryptor } = await kmsService.createCipherPairWithDataKey({
|
const org = await orgDAL.findById(identity.orgId);
|
||||||
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>;
|
|
||||||
try {
|
try {
|
||||||
tokenData = crypto.jwt().verify(oidcJwt, oidcSigningKey.getPublicKey(), {
|
const { decryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
issuer: identityOidcAuth.boundIssuer
|
type: KmsDataKey.Organization,
|
||||||
}) as Record<string, string>;
|
orgId: identity.orgId
|
||||||
} catch (error) {
|
});
|
||||||
if (error instanceof jwt.JsonWebTokenError) {
|
|
||||||
|
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({
|
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;
|
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 ({
|
const attachOidcAuth = async ({
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
import { requestContext } from "@fastify/request-context";
|
||||||
|
|
||||||
import { AccessScope, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas";
|
import { AccessScope, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas";
|
||||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
@@ -19,6 +20,7 @@ import {
|
|||||||
UnauthorizedError
|
UnauthorizedError
|
||||||
} from "@app/lib/errors";
|
} from "@app/lib/errors";
|
||||||
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
||||||
|
import { AuthAttemptAuthMethod, AuthAttemptAuthResult, authAttemptCounter } from "@app/lib/telemetry/metrics";
|
||||||
|
|
||||||
import { ActorType, AuthTokenType } from "../auth/auth-type";
|
import { ActorType, AuthTokenType } from "../auth/auth-type";
|
||||||
import { TIdentityDALFactory } from "../identity/identity-dal";
|
import { TIdentityDALFactory } from "../identity/identity-dal";
|
||||||
@@ -27,6 +29,7 @@ import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identit
|
|||||||
import { TKmsServiceFactory } from "../kms/kms-service";
|
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||||
import { KmsDataKey } from "../kms/kms-types";
|
import { KmsDataKey } from "../kms/kms-types";
|
||||||
import { TMembershipIdentityDALFactory } from "../membership-identity/membership-identity-dal";
|
import { TMembershipIdentityDALFactory } from "../membership-identity/membership-identity-dal";
|
||||||
|
import { TOrgDALFactory } from "../org/org-dal";
|
||||||
import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns";
|
import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns";
|
||||||
import { TIdentityTlsCertAuthDALFactory } from "./identity-tls-cert-auth-dal";
|
import { TIdentityTlsCertAuthDALFactory } from "./identity-tls-cert-auth-dal";
|
||||||
import { TIdentityTlsCertAuthServiceFactory } from "./identity-tls-cert-auth-types";
|
import { TIdentityTlsCertAuthServiceFactory } from "./identity-tls-cert-auth-types";
|
||||||
@@ -42,6 +45,7 @@ type TIdentityTlsCertAuthServiceFactoryDep = {
|
|||||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||||
|
orgDAL: Pick<TOrgDALFactory, "findById">;
|
||||||
};
|
};
|
||||||
|
|
||||||
const parseSubjectDetails = (data: string) => {
|
const parseSubjectDetails = (data: string) => {
|
||||||
@@ -60,9 +64,11 @@ export const identityTlsCertAuthServiceFactory = ({
|
|||||||
membershipIdentityDAL,
|
membershipIdentityDAL,
|
||||||
licenseService,
|
licenseService,
|
||||||
permissionService,
|
permissionService,
|
||||||
kmsService
|
kmsService,
|
||||||
|
orgDAL
|
||||||
}: TIdentityTlsCertAuthServiceFactoryDep): TIdentityTlsCertAuthServiceFactory => {
|
}: TIdentityTlsCertAuthServiceFactoryDep): TIdentityTlsCertAuthServiceFactory => {
|
||||||
const login: TIdentityTlsCertAuthServiceFactory["login"] = async ({ identityId, clientCertificate }) => {
|
const login: TIdentityTlsCertAuthServiceFactory["login"] = async ({ identityId, clientCertificate }) => {
|
||||||
|
const appCfg = getConfig();
|
||||||
const identityTlsCertAuth = await identityTlsCertAuthDAL.findOne({ identityId });
|
const identityTlsCertAuth = await identityTlsCertAuthDAL.findOne({ identityId });
|
||||||
if (!identityTlsCertAuth) {
|
if (!identityTlsCertAuth) {
|
||||||
throw new NotFoundError({
|
throw new NotFoundError({
|
||||||
@@ -73,94 +79,124 @@ export const identityTlsCertAuthServiceFactory = ({
|
|||||||
const identity = await identityDAL.findById(identityTlsCertAuth.identityId);
|
const identity = await identityDAL.findById(identityTlsCertAuth.identityId);
|
||||||
if (!identity) throw new UnauthorizedError({ message: "Identity not found" });
|
if (!identity) throw new UnauthorizedError({ message: "Identity not found" });
|
||||||
|
|
||||||
const { decryptor } = await kmsService.createCipherPairWithDataKey({
|
const org = await orgDAL.findById(identity.orgId);
|
||||||
type: KmsDataKey.Organization,
|
|
||||||
orgId: identity.orgId
|
|
||||||
});
|
|
||||||
|
|
||||||
const caCertificate = decryptor({
|
try {
|
||||||
cipherTextBlob: identityTlsCertAuth.encryptedCaCertificate
|
const { decryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
}).toString();
|
type: KmsDataKey.Organization,
|
||||||
|
orgId: identity.orgId
|
||||||
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."
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (new Date(clientCertificateX509.validTo) < new Date()) {
|
const caCertificate = decryptor({
|
||||||
throw new UnauthorizedError({
|
cipherTextBlob: identityTlsCertAuth.encryptedCaCertificate
|
||||||
message: "Access denied: Certificate has expired."
|
}).toString();
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (new Date(clientCertificateX509.validFrom) > new Date()) {
|
const leafCertificate = extractX509CertFromChain(decodeURIComponent(clientCertificate))?.[0];
|
||||||
throw new UnauthorizedError({
|
if (!leafCertificate) {
|
||||||
message: "Access denied: Certificate not yet valid."
|
throw new BadRequestError({ message: "Missing client certificate" });
|
||||||
});
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const subjectDetails = parseSubjectDetails(clientCertificateX509.subject);
|
const clientCertificateX509 = new crypto.nativeCrypto.X509Certificate(leafCertificate);
|
||||||
if (identityTlsCertAuth.allowedCommonNames) {
|
const caCertificateX509 = new crypto.nativeCrypto.X509Certificate(caCertificate);
|
||||||
const isValidCommonName = identityTlsCertAuth.allowedCommonNames.split(",").includes(subjectDetails.CN);
|
|
||||||
if (!isValidCommonName) {
|
const isValidCertificate = clientCertificateX509.verify(caCertificateX509.publicKey);
|
||||||
|
if (!isValidCertificate)
|
||||||
throw new UnauthorizedError({
|
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
|
if (new Date(clientCertificateX509.validFrom) > new Date()) {
|
||||||
const identityAccessToken = await identityTlsCertAuthDAL.transaction(async (tx) => {
|
throw new UnauthorizedError({
|
||||||
await membershipIdentityDAL.update(
|
message: "Access denied: Certificate not yet valid."
|
||||||
{ scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id },
|
});
|
||||||
{ lastLoginAuthMethod: IdentityAuthMethod.TLS_CERT_AUTH, lastLoginTime: new Date() },
|
}
|
||||||
tx
|
|
||||||
);
|
const subjectDetails = parseSubjectDetails(clientCertificateX509.subject);
|
||||||
const newToken = await identityAccessTokenDAL.create(
|
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,
|
identityId: identityTlsCertAuth.identityId,
|
||||||
isAccessTokenRevoked: false,
|
identityAccessTokenId: identityAccessToken.id,
|
||||||
accessTokenTTL: identityTlsCertAuth.accessTokenTTL,
|
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
|
||||||
accessTokenMaxTTL: identityTlsCertAuth.accessTokenMaxTTL,
|
} as TIdentityAccessTokenJwtPayload,
|
||||||
accessTokenNumUses: 0,
|
appCfg.AUTH_SECRET,
|
||||||
accessTokenNumUsesLimit: identityTlsCertAuth.accessTokenNumUsesLimit,
|
Number(identityAccessToken.accessTokenTTL) === 0
|
||||||
authMethod: IdentityAuthMethod.TLS_CERT_AUTH
|
? undefined
|
||||||
},
|
: {
|
||||||
tx
|
expiresIn: Number(identityAccessToken.accessTokenTTL)
|
||||||
|
}
|
||||||
);
|
);
|
||||||
return newToken;
|
|
||||||
});
|
|
||||||
|
|
||||||
const appCfg = getConfig();
|
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
|
||||||
const accessToken = crypto.jwt().sign(
|
authAttemptCounter.add(1, {
|
||||||
{
|
"infisical.identity.id": identityTlsCertAuth.identityId,
|
||||||
identityId: identityTlsCertAuth.identityId,
|
"infisical.identity.name": identity.name,
|
||||||
identityAccessTokenId: identityAccessToken.id,
|
"infisical.organization.id": org.id,
|
||||||
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
|
"infisical.organization.name": org.name,
|
||||||
} as TIdentityAccessTokenJwtPayload,
|
"infisical.identity.auth_method": AuthAttemptAuthMethod.TLS_CERT_AUTH,
|
||||||
appCfg.AUTH_SECRET,
|
"infisical.identity.auth_result": AuthAttemptAuthResult.SUCCESS,
|
||||||
Number(identityAccessToken.accessTokenTTL) === 0
|
"client.address": requestContext.get("ip"),
|
||||||
? undefined
|
"user_agent.original": requestContext.get("userAgent")
|
||||||
: {
|
});
|
||||||
expiresIn: Number(identityAccessToken.accessTokenTTL)
|
}
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
identityTlsCertAuth,
|
identityTlsCertAuth,
|
||||||
accessToken,
|
accessToken,
|
||||||
identityAccessToken,
|
identityAccessToken,
|
||||||
identity
|
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 ({
|
const attachTlsCertAuth: TIdentityTlsCertAuthServiceFactory["attachTlsCertAuth"] = async ({
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
import { requestContext } from "@fastify/request-context";
|
||||||
|
|
||||||
import { AccessScope, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas";
|
import { AccessScope, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas";
|
||||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
@@ -21,6 +22,7 @@ import {
|
|||||||
} from "@app/lib/errors";
|
} from "@app/lib/errors";
|
||||||
import { checkIPAgainstBlocklist, extractIPDetails, isValidIpOrCidr, TIp } from "@app/lib/ip";
|
import { checkIPAgainstBlocklist, extractIPDetails, isValidIpOrCidr, TIp } from "@app/lib/ip";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
|
import { AuthAttemptAuthMethod, AuthAttemptAuthResult, authAttemptCounter } from "@app/lib/telemetry/metrics";
|
||||||
|
|
||||||
import { ActorType, AuthTokenType } from "../auth/auth-type";
|
import { ActorType, AuthTokenType } from "../auth/auth-type";
|
||||||
import { TIdentityDALFactory } from "../identity/identity-dal";
|
import { TIdentityDALFactory } from "../identity/identity-dal";
|
||||||
@@ -77,6 +79,7 @@ export const identityUaServiceFactory = ({
|
|||||||
identityDAL
|
identityDAL
|
||||||
}: TIdentityUaServiceFactoryDep) => {
|
}: TIdentityUaServiceFactoryDep) => {
|
||||||
const login = async (clientId: string, clientSecret: string, ip: string) => {
|
const login = async (clientId: string, clientSecret: string, ip: string) => {
|
||||||
|
const appCfg = getConfig();
|
||||||
const identityUa = await identityUaDAL.findOne({ clientId });
|
const identityUa = await identityUaDAL.findOne({ clientId });
|
||||||
if (!identityUa) {
|
if (!identityUa) {
|
||||||
throw new UnauthorizedError({
|
throw new UnauthorizedError({
|
||||||
@@ -84,196 +87,226 @@ export const identityUaServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
checkIPAgainstBlocklist({
|
const identity = await identityDAL.findById(identityUa.identityId);
|
||||||
ipAddress: ip,
|
const org = await orgDAL.findById(identity.orgId);
|
||||||
trustedIps: identityUa.clientSecretTrustedIps as TIp[]
|
|
||||||
});
|
|
||||||
|
|
||||||
const LOCKOUT_KEY = `lockout:identity:${identityUa.identityId}:${IdentityAuthMethod.UNIVERSAL_AUTH}:${clientId}`;
|
try {
|
||||||
|
checkIPAgainstBlocklist({
|
||||||
const lockoutRaw = await keyStore.getItem(LOCKOUT_KEY);
|
ipAddress: ip,
|
||||||
|
trustedIps: identityUa.clientSecretTrustedIps as TIp[]
|
||||||
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"
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
const clientSecretPrefix = clientSecret.slice(0, 4);
|
const LOCKOUT_KEY = `lockout:identity:${identityUa.identityId}:${IdentityAuthMethod.UNIVERSAL_AUTH}:${clientId}`;
|
||||||
const clientSecretInfo = await identityUaClientSecretDAL.find({
|
|
||||||
identityUAId: identityUa.id,
|
|
||||||
isClientSecretRevoked: false,
|
|
||||||
clientSecretPrefix
|
|
||||||
});
|
|
||||||
|
|
||||||
let validClientSecretInfo: (typeof clientSecretInfo)[0] | null = null;
|
const lockoutRaw = await keyStore.getItem(LOCKOUT_KEY);
|
||||||
for await (const info of clientSecretInfo) {
|
|
||||||
const isMatch = await crypto.hashing().compareHash(clientSecret, info.clientSecretHash);
|
|
||||||
|
|
||||||
if (isMatch) {
|
let lockout: LockoutObject | undefined;
|
||||||
validClientSecretInfo = info;
|
if (lockoutRaw) {
|
||||||
break;
|
lockout = JSON.parse(lockoutRaw) as LockoutObject;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (!validClientSecretInfo) {
|
if (lockout && lockout.lockedOut) {
|
||||||
if (identityUa.lockoutEnabled) {
|
throw new UnauthorizedError({
|
||||||
let lock: Awaited<ReturnType<typeof keyStore.acquireLock>> | undefined;
|
message: "This identity auth method is temporarily locked, please try again later"
|
||||||
try {
|
});
|
||||||
lock = await keyStore.acquireLock([KeyStorePrefixes.IdentityLockoutLock(LOCKOUT_KEY)], 300, {
|
}
|
||||||
retryCount: 3,
|
|
||||||
retryDelay: 300,
|
|
||||||
retryJitter: 100
|
|
||||||
});
|
|
||||||
|
|
||||||
// Re-fetch the latest lockout data while holding the lock
|
const clientSecretPrefix = clientSecret.slice(0, 4);
|
||||||
const lockoutRawNew = await keyStore.getItem(LOCKOUT_KEY);
|
const clientSecretInfo = await identityUaClientSecretDAL.find({
|
||||||
if (lockoutRawNew) {
|
identityUAId: identityUa.id,
|
||||||
lockout = JSON.parse(lockoutRawNew) as LockoutObject;
|
isClientSecretRevoked: false,
|
||||||
} else {
|
clientSecretPrefix
|
||||||
lockout = {
|
});
|
||||||
lockedOut: false,
|
|
||||||
failedAttempts: 0
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lockout.lockedOut) {
|
let validClientSecretInfo: (typeof clientSecretInfo)[0] | null = null;
|
||||||
throw new UnauthorizedError({
|
for await (const info of clientSecretInfo) {
|
||||||
message: "This identity auth method is temporarily locked, please try again later"
|
const isMatch = await crypto.hashing().compareHash(clientSecret, info.clientSecretHash);
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
lockout.failedAttempts += 1;
|
if (isMatch) {
|
||||||
if (lockout.failedAttempts >= identityUa.lockoutThreshold) {
|
validClientSecretInfo = info;
|
||||||
lockout.lockedOut = true;
|
break;
|
||||||
}
|
|
||||||
|
|
||||||
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" });
|
if (!validClientSecretInfo) {
|
||||||
} else if (lockout) {
|
if (identityUa.lockoutEnabled) {
|
||||||
// If credentials are valid, clear any existing lockout record
|
let lock: Awaited<ReturnType<typeof keyStore.acquireLock>> | undefined;
|
||||||
await keyStore.deleteItem(LOCKOUT_KEY);
|
try {
|
||||||
}
|
lock = await keyStore.acquireLock([KeyStorePrefixes.IdentityLockoutLock(LOCKOUT_KEY)], 300, {
|
||||||
|
retryCount: 3,
|
||||||
|
retryDelay: 300,
|
||||||
|
retryJitter: 100
|
||||||
|
});
|
||||||
|
|
||||||
const { clientSecretTTL, clientSecretNumUses, clientSecretNumUsesLimit } = validClientSecretInfo;
|
// Re-fetch the latest lockout data while holding the lock
|
||||||
if (Number(clientSecretTTL) > 0) {
|
const lockoutRawNew = await keyStore.getItem(LOCKOUT_KEY);
|
||||||
const clientSecretCreated = new Date(validClientSecretInfo.createdAt);
|
if (lockoutRawNew) {
|
||||||
const ttlInMilliseconds = Number(clientSecretTTL) * 1000;
|
lockout = JSON.parse(lockoutRawNew) as LockoutObject;
|
||||||
const currentDate = new Date();
|
} else {
|
||||||
const expirationTime = new Date(clientSecretCreated.getTime() + ttlInMilliseconds);
|
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, {
|
await identityUaClientSecretDAL.updateById(validClientSecretInfo.id, {
|
||||||
isClientSecretRevoked: true
|
isClientSecretRevoked: true
|
||||||
});
|
});
|
||||||
|
|
||||||
throw new UnauthorizedError({
|
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) {
|
const accessTokenTTLParams =
|
||||||
// number of times client secret can be used for
|
Number(identityUa.accessTokenPeriod) === 0
|
||||||
// a login operation reached
|
? {
|
||||||
await identityUaClientSecretDAL.updateById(validClientSecretInfo.id, {
|
accessTokenTTL: identityUa.accessTokenTTL,
|
||||||
isClientSecretRevoked: true
|
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 =
|
const accessToken = crypto.jwt().sign(
|
||||||
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(
|
|
||||||
{
|
{
|
||||||
identityId: identityUa.identityId,
|
identityId: identityUa.identityId,
|
||||||
isAccessTokenRevoked: false,
|
clientSecretId: validClientSecretInfo.id,
|
||||||
identityUAClientSecretId: uaClientSecretDoc.id,
|
identityAccessTokenId: identityAccessToken.id,
|
||||||
accessTokenNumUses: 0,
|
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
|
||||||
accessTokenNumUsesLimit: identityUa.accessTokenNumUsesLimit,
|
} as TIdentityAccessTokenJwtPayload,
|
||||||
accessTokenPeriod: identityUa.accessTokenPeriod,
|
appCfg.AUTH_SECRET,
|
||||||
authMethod: IdentityAuthMethod.UNIVERSAL_AUTH,
|
// akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error
|
||||||
...accessTokenTTLParams
|
Number(identityAccessToken.accessTokenTTL) === 0
|
||||||
},
|
? undefined
|
||||||
tx
|
: {
|
||||||
|
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();
|
return {
|
||||||
const accessToken = crypto.jwt().sign(
|
accessToken,
|
||||||
{
|
identityUa,
|
||||||
identityId: identityUa.identityId,
|
validClientSecretInfo,
|
||||||
clientSecretId: validClientSecretInfo.id,
|
identityAccessToken,
|
||||||
identityAccessTokenId: identityAccessToken.id,
|
identity,
|
||||||
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
|
...accessTokenTTLParams
|
||||||
} as TIdentityAccessTokenJwtPayload,
|
};
|
||||||
appCfg.AUTH_SECRET,
|
} catch (error) {
|
||||||
// akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error
|
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
|
||||||
Number(identityAccessToken.accessTokenTTL) === 0
|
authAttemptCounter.add(1, {
|
||||||
? undefined
|
"infisical.identity.id": identityUa.identityId,
|
||||||
: {
|
"infisical.identity.name": identity.name,
|
||||||
expiresIn: Number(identityAccessToken.accessTokenTTL)
|
"infisical.organization.id": org.id,
|
||||||
}
|
"infisical.organization.name": org.name,
|
||||||
);
|
"infisical.identity.auth_method": AuthAttemptAuthMethod.UNIVERSAL_AUTH,
|
||||||
|
"infisical.identity.auth_result": AuthAttemptAuthResult.FAILURE,
|
||||||
return {
|
"client.address": requestContext.get("ip"),
|
||||||
accessToken,
|
"user_agent.original": requestContext.get("userAgent")
|
||||||
identityUa,
|
});
|
||||||
validClientSecretInfo,
|
}
|
||||||
identityAccessToken,
|
throw error;
|
||||||
identity,
|
}
|
||||||
...accessTokenTTLParams
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const attachUniversalAuth = async ({
|
const attachUniversalAuth = async ({
|
||||||
|
|||||||
Reference in New Issue
Block a user