diff --git a/backend/src/ee/routes/v1/saml-router.ts b/backend/src/ee/routes/v1/saml-router.ts index 51d87f191..00add4adc 100644 --- a/backend/src/ee/routes/v1/saml-router.ts +++ b/backend/src/ee/routes/v1/saml-router.ts @@ -157,26 +157,30 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => { metadata: userMetadata }); - authAttemptCounter.add(1, { - "infisical.user.email": email.toLowerCase(), - "infisical.user.id": user.id, - "infisical.organization.id": organization.id, - "infisical.organization.name": organization.name, - "infisical.auth.method": AuthAttemptAuthMethod.SAML, - "infisical.auth.result": AuthAttemptAuthResult.SUCCESS, - "client.address": requestContext.get("ip"), - "user_agent.original": requestContext.get("userAgent") - }); + if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) { + authAttemptCounter.add(1, { + "infisical.user.email": email.toLowerCase(), + "infisical.user.id": user.id, + "infisical.organization.id": organization.id, + "infisical.organization.name": organization.name, + "infisical.auth.method": AuthAttemptAuthMethod.SAML, + "infisical.auth.result": AuthAttemptAuthResult.SUCCESS, + "client.address": requestContext.get("ip"), + "user_agent.original": requestContext.get("userAgent") + }); + } cb(null, { isUserCompleted, providerAuthToken }); } catch (error) { - authAttemptCounter.add(1, { - "infisical.user.email": email.toLowerCase(), - "infisical.auth.method": AuthAttemptAuthMethod.SAML, - "infisical.auth.result": AuthAttemptAuthResult.FAILURE, - "client.address": requestContext.get("ip"), - "user_agent.original": requestContext.get("userAgent") - }); + if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) { + authAttemptCounter.add(1, { + "infisical.user.email": email.toLowerCase(), + "infisical.auth.method": AuthAttemptAuthMethod.SAML, + "infisical.auth.result": AuthAttemptAuthResult.FAILURE, + "client.address": requestContext.get("ip"), + "user_agent.original": requestContext.get("userAgent") + }); + } logger.error(error); cb(error as Error); diff --git a/backend/src/ee/services/oidc/oidc-config-service.ts b/backend/src/ee/services/oidc/oidc-config-service.ts index 0fa4be4e6..cbe1bed7e 100644 --- a/backend/src/ee/services/oidc/oidc-config-service.ts +++ b/backend/src/ee/services/oidc/oidc-config-service.ts @@ -757,29 +757,33 @@ export const oidcConfigServiceFactory = ({ manageGroupMemberships: oidcCfg.manageGroupMemberships }) .then(({ isUserCompleted, providerAuthToken, user }) => { - authAttemptCounter.add(1, { - "infisical.user.email": claims?.email?.toLowerCase(), - "infisical.user.id": user.id, - "infisical.organization.id": org.id, - "infisical.organization.name": org.name, - "infisical.auth.method": AuthAttemptAuthMethod.OIDC, - "infisical.auth.result": AuthAttemptAuthResult.SUCCESS, - "client.address": requestContext.get("ip"), - "user_agent.original": requestContext.get("userAgent") - }); + if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) { + authAttemptCounter.add(1, { + "infisical.user.email": claims?.email?.toLowerCase(), + "infisical.user.id": user.id, + "infisical.organization.id": org.id, + "infisical.organization.name": org.name, + "infisical.auth.method": AuthAttemptAuthMethod.OIDC, + "infisical.auth.result": AuthAttemptAuthResult.SUCCESS, + "client.address": requestContext.get("ip"), + "user_agent.original": requestContext.get("userAgent") + }); + } cb(null, { isUserCompleted, providerAuthToken }); }) .catch((error) => { - authAttemptCounter.add(1, { - "infisical.user.email": claims?.email?.toLowerCase(), - "infisical.organization.id": org.id, - "infisical.organization.name": org.name, - "infisical.auth.method": AuthAttemptAuthMethod.OIDC, - "infisical.auth.result": AuthAttemptAuthResult.FAILURE, - "client.address": requestContext.get("ip"), - "user_agent.original": requestContext.get("userAgent") - }); + if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) { + authAttemptCounter.add(1, { + "infisical.user.email": claims?.email?.toLowerCase(), + "infisical.organization.id": org.id, + "infisical.organization.name": org.name, + "infisical.auth.method": AuthAttemptAuthMethod.OIDC, + "infisical.auth.result": AuthAttemptAuthResult.FAILURE, + "client.address": requestContext.get("ip"), + "user_agent.original": requestContext.get("userAgent") + }); + } cb(error); }); diff --git a/backend/src/lib/telemetry/metrics.ts b/backend/src/lib/telemetry/metrics.ts index 6748cf426..df6aacaec 100644 --- a/backend/src/lib/telemetry/metrics.ts +++ b/backend/src/lib/telemetry/metrics.ts @@ -8,7 +8,19 @@ export enum AuthAttemptAuthMethod { OIDC = "oidc", GOOGLE = "google", GITHUB = "github", - GITLAB = "gitlab" + GITLAB = "gitlab", + TOKEN_AUTH = "token-auth", + UNIVERSAL_AUTH = "universal-auth", + KUBERNETES_AUTH = "kubernetes-auth", + GCP_AUTH = "gcp-auth", + ALICLOUD_AUTH = "alicloud-auth", + AWS_AUTH = "aws-auth", + AZURE_AUTH = "azure-auth", + TLS_CERT_AUTH = "tls-cert-auth", + OCI_AUTH = "oci-auth", + OIDC_AUTH = "oidc-auth", + JWT_AUTH = "jwt-auth", + LDAP_AUTH = "ldap-auth" } export enum AuthAttemptAuthResult { diff --git a/backend/src/server/routes/v1/sso-router.ts b/backend/src/server/routes/v1/sso-router.ts index 9529c272e..32b09b337 100644 --- a/backend/src/server/routes/v1/sso-router.ts +++ b/backend/src/server/routes/v1/sso-router.ts @@ -76,28 +76,31 @@ export const registerOauthMiddlewares = (server: FastifyZodProvider) => { orgSlug }); - authAttemptCounter.add(1, { - "infisical.user.email": email, - "infisical.user.id": user.id, - "infisical.organization.id": orgId, - "infisical.organization.name": orgName, - "infisical.auth.method": AuthAttemptAuthMethod.GOOGLE, - "infisical.auth.result": AuthAttemptAuthResult.SUCCESS, - "client.address": requestContext.get("ip"), - "user_agent.original": requestContext.get("userAgent") - }); + if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) { + authAttemptCounter.add(1, { + "infisical.user.email": email, + "infisical.user.id": user.id, + "infisical.organization.id": orgId, + "infisical.organization.name": orgName, + "infisical.auth.method": AuthAttemptAuthMethod.GOOGLE, + "infisical.auth.result": AuthAttemptAuthResult.SUCCESS, + "client.address": requestContext.get("ip"), + "user_agent.original": requestContext.get("userAgent") + }); + } cb(null, { isUserCompleted, providerAuthToken }); } catch (error) { logger.error(error); - authAttemptCounter.add(1, { - "infisical.user.email": email, - "infisical.auth.method": AuthAttemptAuthMethod.GOOGLE, - "infisical.auth.result": AuthAttemptAuthResult.FAILURE, - "client.address": requestContext.get("ip"), - "user_agent.original": requestContext.get("userAgent") - }); - + if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) { + authAttemptCounter.add(1, { + "infisical.user.email": email, + "infisical.auth.method": AuthAttemptAuthMethod.GOOGLE, + "infisical.auth.result": AuthAttemptAuthResult.FAILURE, + "client.address": requestContext.get("ip"), + "user_agent.original": requestContext.get("userAgent") + }); + } cb(error as Error, false); } } @@ -144,27 +147,30 @@ export const registerOauthMiddlewares = (server: FastifyZodProvider) => { callbackPort }); - authAttemptCounter.add(1, { - "infisical.user.email": email, - "infisical.user.id": user.id, - "infisical.organization.id": orgId, - "infisical.organization.name": orgName, - "infisical.auth.method": AuthAttemptAuthMethod.GITHUB, - "infisical.auth.result": AuthAttemptAuthResult.SUCCESS, - "client.address": requestContext.get("ip"), - "user_agent.original": requestContext.get("userAgent") - }); + if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) { + authAttemptCounter.add(1, { + "infisical.user.email": email, + "infisical.user.id": user.id, + "infisical.organization.id": orgId, + "infisical.organization.name": orgName, + "infisical.auth.method": AuthAttemptAuthMethod.GITHUB, + "infisical.auth.result": AuthAttemptAuthResult.SUCCESS, + "client.address": requestContext.get("ip"), + "user_agent.original": requestContext.get("userAgent") + }); + } done(null, { isUserCompleted, providerAuthToken, externalProviderAccessToken: accessToken }); } catch (err) { - authAttemptCounter.add(1, { - "infisical.user.email": email, - "infisical.auth.method": AuthAttemptAuthMethod.GITHUB, - "infisical.auth.result": AuthAttemptAuthResult.FAILURE, - "client.address": requestContext.get("ip"), - "user_agent.original": requestContext.get("userAgent") - }); - + if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) { + authAttemptCounter.add(1, { + "infisical.user.email": email, + "infisical.auth.method": AuthAttemptAuthMethod.GITHUB, + "infisical.auth.result": AuthAttemptAuthResult.FAILURE, + "client.address": requestContext.get("ip"), + "user_agent.original": requestContext.get("userAgent") + }); + } logger.error(err); done(err as Error, false); } @@ -204,26 +210,30 @@ export const registerOauthMiddlewares = (server: FastifyZodProvider) => { callbackPort }); - authAttemptCounter.add(1, { - "infisical.user.email": email, - "infisical.user.id": user.id, - "infisical.organization.id": orgId, - "infisical.organization.name": orgName, - "infisical.auth.method": AuthAttemptAuthMethod.GITLAB, - "infisical.auth.result": AuthAttemptAuthResult.SUCCESS, - "client.address": requestContext.get("ip"), - "user_agent.original": requestContext.get("userAgent") - }); + if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) { + authAttemptCounter.add(1, { + "infisical.user.email": email, + "infisical.user.id": user.id, + "infisical.organization.id": orgId, + "infisical.organization.name": orgName, + "infisical.auth.method": AuthAttemptAuthMethod.GITLAB, + "infisical.auth.result": AuthAttemptAuthResult.SUCCESS, + "client.address": requestContext.get("ip"), + "user_agent.original": requestContext.get("userAgent") + }); + } return cb(null, { isUserCompleted, providerAuthToken }); } catch (error) { - authAttemptCounter.add(1, { - "infisical.user.email": email, - "infisical.auth.method": AuthAttemptAuthMethod.GITLAB, - "infisical.auth.result": AuthAttemptAuthResult.FAILURE, - "client.address": requestContext.get("ip"), - "user_agent.original": requestContext.get("userAgent") - }); + if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) { + authAttemptCounter.add(1, { + "infisical.user.email": email, + "infisical.auth.method": AuthAttemptAuthMethod.GITLAB, + "infisical.auth.result": AuthAttemptAuthResult.FAILURE, + "client.address": requestContext.get("ip"), + "user_agent.original": requestContext.get("userAgent") + }); + } logger.error(error); cb(error as Error, false); diff --git a/backend/src/services/auth/auth-login-service.ts b/backend/src/services/auth/auth-login-service.ts index 47e1b687f..b9c759703 100644 --- a/backend/src/services/auth/auth-login-service.ts +++ b/backend/src/services/auth/auth-login-service.ts @@ -386,6 +386,8 @@ export const authLoginServiceFactory = ({ providerAuthToken?: string; captchaToken?: string; }) => { + const appCfg = getConfig(); + try { const usersByUsername = await userDAL.findUserEncKeyByUsername({ username: email @@ -440,15 +442,17 @@ export const authLoginServiceFactory = ({ organizationId }); - authAttemptCounter.add(1, { - "infisical.organization.id": organizationId, - "infisical.user.email": email, - "infisical.user.id": userEnc.userId, - "infisical.auth.method": AuthAttemptAuthMethod.EMAIL, - "infisical.auth.result": AuthAttemptAuthResult.SUCCESS, - "client.address": ip, - "user_agent.original": userAgent - }); + if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) { + authAttemptCounter.add(1, { + "infisical.organization.id": organizationId, + "infisical.user.email": email, + "infisical.user.id": userEnc.userId, + "infisical.auth.method": AuthAttemptAuthMethod.EMAIL, + "infisical.auth.result": AuthAttemptAuthResult.SUCCESS, + "client.address": ip, + "user_agent.original": userAgent + }); + } return { tokens: { @@ -458,13 +462,15 @@ export const authLoginServiceFactory = ({ user: userEnc } as const; } catch (error) { - authAttemptCounter.add(1, { - "infisical.user.email": email, - "infisical.auth.method": AuthAttemptAuthMethod.EMAIL, - "infisical.auth.result": AuthAttemptAuthResult.FAILURE, - "client.address": ip, - "user_agent.original": userAgent - }); + if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) { + authAttemptCounter.add(1, { + "infisical.user.email": email, + "infisical.auth.method": AuthAttemptAuthMethod.EMAIL, + "infisical.auth.result": AuthAttemptAuthResult.FAILURE, + "client.address": ip, + "user_agent.original": userAgent + }); + } throw error; } diff --git a/backend/src/services/identity-alicloud-auth/identity-alicloud-auth-service.ts b/backend/src/services/identity-alicloud-auth/identity-alicloud-auth-service.ts index c6f6f1376..525da1e10 100644 --- a/backend/src/services/identity-alicloud-auth/identity-alicloud-auth-service.ts +++ b/backend/src/services/identity-alicloud-auth/identity-alicloud-auth-service.ts @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { ForbiddenError } from "@casl/ability"; +import { requestContext } from "@fastify/request-context"; import { AxiosError } from "axios"; import { AccessScope, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas"; @@ -22,6 +23,7 @@ import { } from "@app/lib/errors"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { logger } from "@app/lib/logger"; +import { AuthAttemptAuthMethod, AuthAttemptAuthResult, authAttemptCounter } from "@app/lib/telemetry/metrics"; import { ActorType, AuthTokenType } from "../auth/auth-type"; import { TIdentityDALFactory } from "../identity/identity-dal"; @@ -65,6 +67,7 @@ export const identityAliCloudAuthServiceFactory = ({ orgDAL }: TIdentityAliCloudAuthServiceFactoryDep) => { const login = async ({ identityId, ...params }: TLoginAliCloudAuthDTO) => { + const appCfg = getConfig(); const identityAliCloudAuth = await identityAliCloudAuthDAL.findOne({ identityId }); if (!identityAliCloudAuth) { throw new NotFoundError({ @@ -75,73 +78,103 @@ export const identityAliCloudAuthServiceFactory = ({ const identity = await identityDAL.findById(identityAliCloudAuth.identityId); if (!identity) throw new UnauthorizedError({ message: "Identity not found" }); - const requestUrl = new URL("https://sts.aliyuncs.com"); + const org = await orgDAL.findById(identity.orgId); - for (const key of Object.keys(params)) { - requestUrl.searchParams.set(key, (params as Record)[key]); - } + try { + const requestUrl = new URL("https://sts.aliyuncs.com"); - const { data } = await request.get(requestUrl.toString()).catch((err: AxiosError) => { - logger.error(err.response, "AliCloudIdentityLogin: Failed to authenticate with Alibaba Cloud"); - throw err; - }); + for (const key of Object.keys(params)) { + requestUrl.searchParams.set(key, (params as Record)[key]); + } - if (identityAliCloudAuth.allowedArns) { - // In the future we could do partial checks for role ARNs - const isAccountAllowed = identityAliCloudAuth.allowedArns.split(",").some((arn) => arn.trim() === data.Arn); + const { data } = await request.get(requestUrl.toString()).catch((err: AxiosError) => { + logger.error(err.response, "AliCloudIdentityLogin: Failed to authenticate with Alibaba Cloud"); + throw err; + }); - if (!isAccountAllowed) - throw new UnauthorizedError({ - message: "Access denied: Alibaba Cloud account ARN not allowed." - }); - } + if (identityAliCloudAuth.allowedArns) { + // In the future we could do partial checks for role ARNs + const isAccountAllowed = identityAliCloudAuth.allowedArns.split(",").some((arn) => arn.trim() === data.Arn); - // Generate the token - const identityAccessToken = await identityAliCloudAuthDAL.transaction(async (tx) => { - await membershipIdentityDAL.update( - { scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id }, - { - lastLoginAuthMethod: IdentityAuthMethod.ALICLOUD_AUTH, - lastLoginTime: new Date() - }, - tx - ); - const newToken = await identityAccessTokenDAL.create( + if (!isAccountAllowed) + throw new UnauthorizedError({ + message: "Access denied: Alibaba Cloud account ARN not allowed." + }); + } + + // Generate the token + const identityAccessToken = await identityAliCloudAuthDAL.transaction(async (tx) => { + await membershipIdentityDAL.update( + { scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id }, + { + lastLoginAuthMethod: IdentityAuthMethod.ALICLOUD_AUTH, + lastLoginTime: new Date() + }, + tx + ); + const newToken = await identityAccessTokenDAL.create( + { + identityId: identityAliCloudAuth.identityId, + isAccessTokenRevoked: false, + accessTokenTTL: identityAliCloudAuth.accessTokenTTL, + accessTokenMaxTTL: identityAliCloudAuth.accessTokenMaxTTL, + accessTokenNumUses: 0, + accessTokenNumUsesLimit: identityAliCloudAuth.accessTokenNumUsesLimit, + authMethod: IdentityAuthMethod.ALICLOUD_AUTH + }, + tx + ); + return newToken; + }); + + const accessToken = crypto.jwt().sign( { identityId: identityAliCloudAuth.identityId, - isAccessTokenRevoked: false, - accessTokenTTL: identityAliCloudAuth.accessTokenTTL, - accessTokenMaxTTL: identityAliCloudAuth.accessTokenMaxTTL, - accessTokenNumUses: 0, - accessTokenNumUsesLimit: identityAliCloudAuth.accessTokenNumUsesLimit, - authMethod: IdentityAuthMethod.ALICLOUD_AUTH - }, - tx + identityAccessTokenId: identityAccessToken.id, + authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN + } as TIdentityAccessTokenJwtPayload, + appCfg.AUTH_SECRET, + Number(identityAccessToken.accessTokenTTL) === 0 + ? undefined + : { + expiresIn: Number(identityAccessToken.accessTokenTTL) + } ); - return newToken; - }); - const appCfg = getConfig(); - const accessToken = crypto.jwt().sign( - { - identityId: identityAliCloudAuth.identityId, - identityAccessTokenId: identityAccessToken.id, - authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN - } as TIdentityAccessTokenJwtPayload, - appCfg.AUTH_SECRET, - Number(identityAccessToken.accessTokenTTL) === 0 - ? undefined - : { - expiresIn: Number(identityAccessToken.accessTokenTTL) - } - ); + if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) { + authAttemptCounter.add(1, { + "infisical.identity.id": identityAliCloudAuth.identityId, + "infisical.identity.name": identity.name, + "infisical.organization.id": org.id, + "infisical.organization.name": org.name, + "infisical.identity.auth_method": AuthAttemptAuthMethod.ALICLOUD_AUTH, + "infisical.identity.auth_result": AuthAttemptAuthResult.SUCCESS, + "client.address": requestContext.get("ip"), + "user_agent.original": requestContext.get("userAgent") + }); + } - return { - identityAliCloudAuth, - accessToken, - identityAccessToken, - identity - }; + return { + identityAliCloudAuth, + accessToken, + identityAccessToken, + identity + }; + } catch (error) { + if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) { + authAttemptCounter.add(1, { + "infisical.identity.id": identityAliCloudAuth.identityId, + "infisical.identity.name": identity.name, + "infisical.organization.id": org.id, + "infisical.organization.name": org.name, + "infisical.identity.auth_method": AuthAttemptAuthMethod.ALICLOUD_AUTH, + "infisical.identity.auth_result": AuthAttemptAuthResult.FAILURE, + "client.address": requestContext.get("ip"), + "user_agent.original": requestContext.get("userAgent") + }); + } + throw error; + } }; const attachAliCloudAuth = async ({ diff --git a/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts b/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts index e61e8296b..81fab3fde 100644 --- a/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts +++ b/backend/src/services/identity-aws-auth/identity-aws-auth-service.ts @@ -1,5 +1,6 @@ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access */ import { ForbiddenError } from "@casl/ability"; +import { requestContext } from "@fastify/request-context"; import axios from "axios"; import RE2 from "re2"; @@ -22,6 +23,7 @@ import { } from "@app/lib/errors"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { logger } from "@app/lib/logger"; +import { AuthAttemptAuthMethod, AuthAttemptAuthResult, authAttemptCounter } from "@app/lib/telemetry/metrics"; import { ActorType, AuthTokenType } from "../auth/auth-type"; import { TIdentityDALFactory } from "../identity/identity-dal"; @@ -98,6 +100,7 @@ export const identityAwsAuthServiceFactory = ({ orgDAL }: TIdentityAwsAuthServiceFactoryDep) => { const login = async ({ identityId, iamHttpRequestMethod, iamRequestBody, iamRequestHeaders }: TLoginAwsAuthDTO) => { + const appCfg = getConfig(); const identityAwsAuth = await identityAwsAuthDAL.findOne({ identityId }); if (!identityAwsAuth) { throw new NotFoundError({ message: "AWS auth method not found for identity, did you configure AWS auth?" }); @@ -106,127 +109,156 @@ export const identityAwsAuthServiceFactory = ({ const identity = await identityDAL.findById(identityAwsAuth.identityId); if (!identity) throw new UnauthorizedError({ message: "Identity not found" }); - const headers: TAwsGetCallerIdentityHeaders = JSON.parse(Buffer.from(iamRequestHeaders, "base64").toString()); - const body: string = Buffer.from(iamRequestBody, "base64").toString(); + const org = await orgDAL.findById(identity.orgId); + try { + const headers: TAwsGetCallerIdentityHeaders = JSON.parse(Buffer.from(iamRequestHeaders, "base64").toString()); + const body: string = Buffer.from(iamRequestBody, "base64").toString(); - const authHeader = headers.Authorization || headers.authorization; - const region = authHeader ? awsRegionFromHeader(authHeader) : null; + const authHeader = headers.Authorization || headers.authorization; + const region = authHeader ? awsRegionFromHeader(authHeader) : null; - if (!isValidAwsRegion(region)) { - throw new BadRequestError({ message: "Invalid AWS region" }); - } + if (!isValidAwsRegion(region)) { + throw new BadRequestError({ message: "Invalid AWS region" }); + } - const url = region ? `https://sts.${region}.amazonaws.com` : identityAwsAuth.stsEndpoint; + const url = region ? `https://sts.${region}.amazonaws.com` : identityAwsAuth.stsEndpoint; - const { - data: { - GetCallerIdentityResponse: { - GetCallerIdentityResult: { Account, Arn, UserId } + const { + data: { + GetCallerIdentityResponse: { + GetCallerIdentityResult: { Account, Arn, UserId } + } + } + }: { data: TGetCallerIdentityResponse } = await axios({ + method: iamHttpRequestMethod, + url, + headers, + data: body + }); + + if (identityAwsAuth.allowedAccountIds) { + // validate if Account is in the list of allowed Account IDs + + const isAccountAllowed = identityAwsAuth.allowedAccountIds + .split(",") + .map((accountId) => accountId.trim()) + .some((accountId) => accountId === Account); + + if (!isAccountAllowed) + throw new UnauthorizedError({ + message: "Access denied: AWS account ID not allowed." + }); + } + + if (identityAwsAuth.allowedPrincipalArns) { + // validate if Arn is in the list of allowed Principal ARNs + + const formattedArn = extractPrincipalArn(Arn); + + const isArnAllowed = identityAwsAuth.allowedPrincipalArns + .split(",") + .map((principalArn) => principalArn.trim()) + .some((principalArn) => { + // convert wildcard ARN to a regular expression: "arn:aws:iam::123456789012:*" -> "^arn:aws:iam::123456789012:.*$" + // considers exact matches + wildcard matches + // heavily validated in router + const regex = new RE2(`^${principalArn.replaceAll("*", ".*")}$`); + return regex.test(formattedArn) || regex.test(extractPrincipalArn(Arn, true)); + }); + + if (!isArnAllowed) { + logger.error( + `AWS Auth Login: AWS principal ARN not allowed [principal-arn=${formattedArn}] [raw-arn=${Arn}] [identity-id=${identity.id}]` + ); + + throw new UnauthorizedError({ + message: `Access denied: AWS principal ARN not allowed. [principal-arn=${formattedArn}]` + }); } } - }: { data: TGetCallerIdentityResponse } = await axios({ - method: iamHttpRequestMethod, - url, - headers, - data: body - }); - if (identityAwsAuth.allowedAccountIds) { - // validate if Account is in the list of allowed Account IDs - - const isAccountAllowed = identityAwsAuth.allowedAccountIds - .split(",") - .map((accountId) => accountId.trim()) - .some((accountId) => accountId === Account); - - if (!isAccountAllowed) - throw new UnauthorizedError({ - message: "Access denied: AWS account ID not allowed." - }); - } - - if (identityAwsAuth.allowedPrincipalArns) { - // validate if Arn is in the list of allowed Principal ARNs - - const formattedArn = extractPrincipalArn(Arn); - - const isArnAllowed = identityAwsAuth.allowedPrincipalArns - .split(",") - .map((principalArn) => principalArn.trim()) - .some((principalArn) => { - // convert wildcard ARN to a regular expression: "arn:aws:iam::123456789012:*" -> "^arn:aws:iam::123456789012:.*$" - // considers exact matches + wildcard matches - // heavily validated in router - const regex = new RE2(`^${principalArn.replaceAll("*", ".*")}$`); - return regex.test(formattedArn) || regex.test(extractPrincipalArn(Arn, true)); - }); - - if (!isArnAllowed) { - logger.error( - `AWS Auth Login: AWS principal ARN not allowed [principal-arn=${formattedArn}] [raw-arn=${Arn}] [identity-id=${identity.id}]` + const identityAccessToken = await identityAwsAuthDAL.transaction(async (tx) => { + await membershipIdentityDAL.update( + { scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id }, + { + lastLoginAuthMethod: IdentityAuthMethod.AWS_AUTH, + lastLoginTime: new Date() + }, + tx ); + const newToken = await identityAccessTokenDAL.create( + { + identityId: identityAwsAuth.identityId, + isAccessTokenRevoked: false, + accessTokenTTL: identityAwsAuth.accessTokenTTL, + accessTokenMaxTTL: identityAwsAuth.accessTokenMaxTTL, + accessTokenNumUses: 0, + accessTokenNumUsesLimit: identityAwsAuth.accessTokenNumUsesLimit, + authMethod: IdentityAuthMethod.AWS_AUTH + }, + tx + ); + return newToken; + }); - throw new UnauthorizedError({ - message: `Access denied: AWS principal ARN not allowed. [principal-arn=${formattedArn}]` - }); - } - } - - const identityAccessToken = await identityAwsAuthDAL.transaction(async (tx) => { - await membershipIdentityDAL.update( - { scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id }, - { - lastLoginAuthMethod: IdentityAuthMethod.AWS_AUTH, - lastLoginTime: new Date() - }, - tx - ); - const newToken = await identityAccessTokenDAL.create( + const splitArn = extractPrincipalArnEntity(Arn); + const accessToken = crypto.jwt().sign( { identityId: identityAwsAuth.identityId, - isAccessTokenRevoked: false, - accessTokenTTL: identityAwsAuth.accessTokenTTL, - accessTokenMaxTTL: identityAwsAuth.accessTokenMaxTTL, - accessTokenNumUses: 0, - accessTokenNumUsesLimit: identityAwsAuth.accessTokenNumUsesLimit, - authMethod: IdentityAuthMethod.AWS_AUTH - }, - tx + identityAccessTokenId: identityAccessToken.id, + authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN, + identityAuth: { + aws: { + accountId: Account, + arn: Arn, + userId: UserId, + + // Derived from ARN + partition: splitArn.Partition, + service: splitArn.Service, + resourceType: splitArn.Type, + resourceName: splitArn.FriendlyName + } + } + } as TIdentityAccessTokenJwtPayload, + appCfg.AUTH_SECRET, + // akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error + Number(identityAccessToken.accessTokenTTL) === 0 + ? undefined + : { + expiresIn: Number(identityAccessToken.accessTokenTTL) + } ); - return newToken; - }); - const appCfg = getConfig(); - const splitArn = extractPrincipalArnEntity(Arn); - const accessToken = crypto.jwt().sign( - { - identityId: identityAwsAuth.identityId, - identityAccessTokenId: identityAccessToken.id, - authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN, - identityAuth: { - aws: { - accountId: Account, - arn: Arn, - userId: UserId, + if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) { + authAttemptCounter.add(1, { + "infisical.identity.id": identityAwsAuth.identityId, + "infisical.identity.name": identity.name, + "infisical.organization.id": org.id, + "infisical.organization.name": org.name, + "infisical.identity.auth_method": AuthAttemptAuthMethod.AWS_AUTH, + "infisical.identity.auth_result": AuthAttemptAuthResult.SUCCESS, + "client.address": requestContext.get("ip"), + "user_agent.original": requestContext.get("userAgent") + }); + } - // Derived from ARN - partition: splitArn.Partition, - service: splitArn.Service, - resourceType: splitArn.Type, - resourceName: splitArn.FriendlyName - } - } - } as TIdentityAccessTokenJwtPayload, - appCfg.AUTH_SECRET, - // akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error - Number(identityAccessToken.accessTokenTTL) === 0 - ? undefined - : { - expiresIn: Number(identityAccessToken.accessTokenTTL) - } - ); - - return { accessToken, identityAwsAuth, identityAccessToken, identity }; + return { accessToken, identityAwsAuth, identityAccessToken, identity }; + } catch (error) { + if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) { + authAttemptCounter.add(1, { + "infisical.identity.id": identityAwsAuth.identityId, + "infisical.identity.name": identity.name, + "infisical.organization.id": org.id, + "infisical.organization.name": org.name, + "infisical.identity.auth_method": AuthAttemptAuthMethod.AWS_AUTH, + "infisical.identity.auth_result": AuthAttemptAuthResult.FAILURE, + "client.address": requestContext.get("ip"), + "user_agent.original": requestContext.get("userAgent") + }); + } + throw error; + } }; const attachAwsAuth = async ({ diff --git a/backend/src/services/identity-azure-auth/identity-azure-auth-service.ts b/backend/src/services/identity-azure-auth/identity-azure-auth-service.ts index f75aeba4f..17f274e7c 100644 --- a/backend/src/services/identity-azure-auth/identity-azure-auth-service.ts +++ b/backend/src/services/identity-azure-auth/identity-azure-auth-service.ts @@ -1,4 +1,5 @@ import { ForbiddenError } from "@casl/ability"; +import { requestContext } from "@fastify/request-context"; import { AccessScope, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; @@ -18,6 +19,7 @@ import { UnauthorizedError } from "@app/lib/errors"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; +import { AuthAttemptAuthMethod, AuthAttemptAuthResult, authAttemptCounter } from "@app/lib/telemetry/metrics"; import { ActorType, AuthTokenType } from "../auth/auth-type"; import { TIdentityDALFactory } from "../identity/identity-dal"; @@ -61,6 +63,7 @@ export const identityAzureAuthServiceFactory = ({ orgDAL }: TIdentityAzureAuthServiceFactoryDep) => { const login = async ({ identityId, jwt: azureJwt }: TLoginAzureAuthDTO) => { + const appCfg = getConfig(); const identityAzureAuth = await identityAzureAuthDAL.findOne({ identityId }); if (!identityAzureAuth) { throw new NotFoundError({ message: "Azure auth method not found for identity, did you configure Azure Auth?" }); @@ -69,69 +72,99 @@ export const identityAzureAuthServiceFactory = ({ const identity = await identityDAL.findById(identityAzureAuth.identityId); if (!identity) throw new UnauthorizedError({ message: "Identity not found" }); - const azureIdentity = await validateAzureIdentity({ - tenantId: identityAzureAuth.tenantId, - resource: identityAzureAuth.resource, - jwt: azureJwt - }); + const org = await orgDAL.findById(identity.orgId); - if (azureIdentity.tid !== identityAzureAuth.tenantId) - throw new UnauthorizedError({ message: "Tenant ID mismatch" }); + try { + const azureIdentity = await validateAzureIdentity({ + tenantId: identityAzureAuth.tenantId, + resource: identityAzureAuth.resource, + jwt: azureJwt + }); - if (identityAzureAuth.allowedServicePrincipalIds) { - // validate if the service principal id is in the list of allowed service principal ids + if (azureIdentity.tid !== identityAzureAuth.tenantId) + throw new UnauthorizedError({ message: "Tenant ID mismatch" }); - const isServicePrincipalAllowed = identityAzureAuth.allowedServicePrincipalIds - .split(",") - .map((servicePrincipalId) => servicePrincipalId.trim()) - .some((servicePrincipalId) => servicePrincipalId === azureIdentity.oid); + if (identityAzureAuth.allowedServicePrincipalIds) { + // validate if the service principal id is in the list of allowed service principal ids - if (!isServicePrincipalAllowed) { - throw new UnauthorizedError({ message: `Service principal '${azureIdentity.oid}' not allowed` }); + const isServicePrincipalAllowed = identityAzureAuth.allowedServicePrincipalIds + .split(",") + .map((servicePrincipalId) => servicePrincipalId.trim()) + .some((servicePrincipalId) => servicePrincipalId === azureIdentity.oid); + + if (!isServicePrincipalAllowed) { + throw new UnauthorizedError({ message: `Service principal '${azureIdentity.oid}' not allowed` }); + } } - } - const identityAccessToken = await identityAzureAuthDAL.transaction(async (tx) => { - await membershipIdentityDAL.update( - { scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id }, - { - lastLoginAuthMethod: IdentityAuthMethod.AZURE_AUTH, - lastLoginTime: new Date() - }, - tx - ); - const newToken = await identityAccessTokenDAL.create( + const identityAccessToken = await identityAzureAuthDAL.transaction(async (tx) => { + await membershipIdentityDAL.update( + { scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id }, + { + lastLoginAuthMethod: IdentityAuthMethod.AZURE_AUTH, + lastLoginTime: new Date() + }, + tx + ); + const newToken = await identityAccessTokenDAL.create( + { + identityId: identityAzureAuth.identityId, + isAccessTokenRevoked: false, + accessTokenTTL: identityAzureAuth.accessTokenTTL, + accessTokenMaxTTL: identityAzureAuth.accessTokenMaxTTL, + accessTokenNumUses: 0, + accessTokenNumUsesLimit: identityAzureAuth.accessTokenNumUsesLimit, + authMethod: IdentityAuthMethod.AZURE_AUTH + }, + tx + ); + return newToken; + }); + + const accessToken = crypto.jwt().sign( { identityId: identityAzureAuth.identityId, - isAccessTokenRevoked: false, - accessTokenTTL: identityAzureAuth.accessTokenTTL, - accessTokenMaxTTL: identityAzureAuth.accessTokenMaxTTL, - accessTokenNumUses: 0, - accessTokenNumUsesLimit: identityAzureAuth.accessTokenNumUsesLimit, - authMethod: IdentityAuthMethod.AZURE_AUTH - }, - tx + identityAccessTokenId: identityAccessToken.id, + authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN + } as TIdentityAccessTokenJwtPayload, + appCfg.AUTH_SECRET, + // akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error + Number(identityAccessToken.accessTokenTTL) === 0 + ? undefined + : { + expiresIn: Number(identityAccessToken.accessTokenTTL) + } ); - return newToken; - }); - const appCfg = getConfig(); - const accessToken = crypto.jwt().sign( - { - identityId: identityAzureAuth.identityId, - identityAccessTokenId: identityAccessToken.id, - authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN - } as TIdentityAccessTokenJwtPayload, - appCfg.AUTH_SECRET, - // akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error - Number(identityAccessToken.accessTokenTTL) === 0 - ? undefined - : { - expiresIn: Number(identityAccessToken.accessTokenTTL) - } - ); + if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) { + authAttemptCounter.add(1, { + "infisical.identity.id": identityAzureAuth.identityId, + "infisical.identity.name": identity.name, + "infisical.organization.id": org.id, + "infisical.organization.name": org.name, + "infisical.identity.auth_method": AuthAttemptAuthMethod.AZURE_AUTH, + "infisical.identity.auth_result": AuthAttemptAuthResult.SUCCESS, + "client.address": requestContext.get("ip"), + "user_agent.original": requestContext.get("userAgent") + }); + } - return { accessToken, identityAzureAuth, identityAccessToken, identity }; + return { accessToken, identityAzureAuth, identityAccessToken, identity }; + } catch (error) { + if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) { + authAttemptCounter.add(1, { + "infisical.identity.id": identityAzureAuth.identityId, + "infisical.identity.name": identity.name, + "infisical.organization.id": org.id, + "infisical.organization.name": org.name, + "infisical.identity.auth_method": AuthAttemptAuthMethod.AZURE_AUTH, + "infisical.identity.auth_result": AuthAttemptAuthResult.FAILURE, + "client.address": requestContext.get("ip"), + "user_agent.original": requestContext.get("userAgent") + }); + } + throw error; + } }; const attachAzureAuth = async ({ diff --git a/backend/src/services/identity-gcp-auth/identity-gcp-auth-service.ts b/backend/src/services/identity-gcp-auth/identity-gcp-auth-service.ts index 67adb6c1e..3e0035e82 100644 --- a/backend/src/services/identity-gcp-auth/identity-gcp-auth-service.ts +++ b/backend/src/services/identity-gcp-auth/identity-gcp-auth-service.ts @@ -1,4 +1,5 @@ import { ForbiddenError } from "@casl/ability"; +import { requestContext } from "@fastify/request-context"; import { AccessScope, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; @@ -18,6 +19,7 @@ import { UnauthorizedError } from "@app/lib/errors"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; +import { AuthAttemptAuthMethod, AuthAttemptAuthResult, authAttemptCounter } from "@app/lib/telemetry/metrics"; import { ActorType, AuthTokenType } from "../auth/auth-type"; import { TIdentityDALFactory } from "../identity/identity-dal"; @@ -59,6 +61,7 @@ export const identityGcpAuthServiceFactory = ({ orgDAL }: TIdentityGcpAuthServiceFactoryDep) => { const login = async ({ identityId, jwt: gcpJwt }: TLoginGcpAuthDTO) => { + const appCfg = getConfig(); const identityGcpAuth = await identityGcpAuthDAL.findOne({ identityId }); if (!identityGcpAuth) { throw new NotFoundError({ message: "GCP auth method not found for identity, did you configure GCP auth?" }); @@ -67,108 +70,140 @@ export const identityGcpAuthServiceFactory = ({ const identity = await identityDAL.findById(identityGcpAuth.identityId); if (!identity) throw new UnauthorizedError({ message: "Identity not found" }); - let gcpIdentityDetails: TGcpIdentityDetails; - switch (identityGcpAuth.type) { - case "gce": { - gcpIdentityDetails = await validateIdTokenIdentity({ - identityId, - jwt: gcpJwt - }); - break; + const org = await orgDAL.findById(identity.orgId); + try { + let gcpIdentityDetails: TGcpIdentityDetails; + switch (identityGcpAuth.type) { + case "gce": { + gcpIdentityDetails = await validateIdTokenIdentity({ + identityId, + jwt: gcpJwt + }); + break; + } + case "iam": { + gcpIdentityDetails = await validateIamIdentity({ + identityId, + jwt: gcpJwt + }); + break; + } + default: { + throw new BadRequestError({ message: "Invalid GCP Auth type" }); + } } - case "iam": { - gcpIdentityDetails = await validateIamIdentity({ - identityId, - jwt: gcpJwt - }); - break; + + if (identityGcpAuth.allowedServiceAccounts) { + // validate if the service account is in the list of allowed service accounts + + const isServiceAccountAllowed = identityGcpAuth.allowedServiceAccounts + .split(",") + .map((serviceAccount) => serviceAccount.trim()) + .some((serviceAccount) => serviceAccount === gcpIdentityDetails.email); + + if (!isServiceAccountAllowed) + throw new UnauthorizedError({ + message: "Access denied: GCP service account not allowed." + }); } - default: { - throw new BadRequestError({ message: "Invalid GCP Auth type" }); + + if ( + identityGcpAuth.type === "gce" && + identityGcpAuth.allowedProjects && + gcpIdentityDetails.computeEngineDetails + ) { + // validate if the project that the service account belongs to is in the list of allowed projects + + const isProjectAllowed = identityGcpAuth.allowedProjects + .split(",") + .map((project) => project.trim()) + .some((project) => project === gcpIdentityDetails.computeEngineDetails?.project_id); + + if (!isProjectAllowed) + throw new UnauthorizedError({ + message: "Access denied: GCP project not allowed." + }); } - } - if (identityGcpAuth.allowedServiceAccounts) { - // validate if the service account is in the list of allowed service accounts + if (identityGcpAuth.type === "gce" && identityGcpAuth.allowedZones && gcpIdentityDetails.computeEngineDetails) { + const isZoneAllowed = identityGcpAuth.allowedZones + .split(",") + .map((zone) => zone.trim()) + .some((zone) => zone === gcpIdentityDetails.computeEngineDetails?.zone); - const isServiceAccountAllowed = identityGcpAuth.allowedServiceAccounts - .split(",") - .map((serviceAccount) => serviceAccount.trim()) - .some((serviceAccount) => serviceAccount === gcpIdentityDetails.email); + if (!isZoneAllowed) + throw new UnauthorizedError({ + message: "Access denied: GCP zone not allowed." + }); + } - if (!isServiceAccountAllowed) - throw new UnauthorizedError({ - message: "Access denied: GCP service account not allowed." - }); - } - - if (identityGcpAuth.type === "gce" && identityGcpAuth.allowedProjects && gcpIdentityDetails.computeEngineDetails) { - // validate if the project that the service account belongs to is in the list of allowed projects - - const isProjectAllowed = identityGcpAuth.allowedProjects - .split(",") - .map((project) => project.trim()) - .some((project) => project === gcpIdentityDetails.computeEngineDetails?.project_id); - - if (!isProjectAllowed) - throw new UnauthorizedError({ - message: "Access denied: GCP project not allowed." - }); - } - - if (identityGcpAuth.type === "gce" && identityGcpAuth.allowedZones && gcpIdentityDetails.computeEngineDetails) { - const isZoneAllowed = identityGcpAuth.allowedZones - .split(",") - .map((zone) => zone.trim()) - .some((zone) => zone === gcpIdentityDetails.computeEngineDetails?.zone); - - if (!isZoneAllowed) - throw new UnauthorizedError({ - message: "Access denied: GCP zone not allowed." - }); - } - - const identityAccessToken = await identityGcpAuthDAL.transaction(async (tx) => { - await membershipIdentityDAL.update( - { scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id }, - { - lastLoginAuthMethod: IdentityAuthMethod.GCP_AUTH, - lastLoginTime: new Date() - }, - tx - ); - const newToken = await identityAccessTokenDAL.create( + const identityAccessToken = await identityGcpAuthDAL.transaction(async (tx) => { + await membershipIdentityDAL.update( + { scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id }, + { + lastLoginAuthMethod: IdentityAuthMethod.GCP_AUTH, + lastLoginTime: new Date() + }, + tx + ); + const newToken = await identityAccessTokenDAL.create( + { + identityId: identityGcpAuth.identityId, + isAccessTokenRevoked: false, + accessTokenTTL: identityGcpAuth.accessTokenTTL, + accessTokenMaxTTL: identityGcpAuth.accessTokenMaxTTL, + accessTokenNumUses: 0, + accessTokenNumUsesLimit: identityGcpAuth.accessTokenNumUsesLimit, + authMethod: IdentityAuthMethod.GCP_AUTH + }, + tx + ); + return newToken; + }); + const accessToken = crypto.jwt().sign( { identityId: identityGcpAuth.identityId, - isAccessTokenRevoked: false, - accessTokenTTL: identityGcpAuth.accessTokenTTL, - accessTokenMaxTTL: identityGcpAuth.accessTokenMaxTTL, - accessTokenNumUses: 0, - accessTokenNumUsesLimit: identityGcpAuth.accessTokenNumUsesLimit, - authMethod: IdentityAuthMethod.GCP_AUTH - }, - tx + identityAccessTokenId: identityAccessToken.id, + authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN + } as TIdentityAccessTokenJwtPayload, + appCfg.AUTH_SECRET, + // akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error + Number(identityAccessToken.accessTokenTTL) === 0 + ? undefined + : { + expiresIn: Number(identityAccessToken.accessTokenTTL) + } ); - return newToken; - }); - const appCfg = getConfig(); - const accessToken = crypto.jwt().sign( - { - identityId: identityGcpAuth.identityId, - identityAccessTokenId: identityAccessToken.id, - authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN - } as TIdentityAccessTokenJwtPayload, - appCfg.AUTH_SECRET, - // akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error - Number(identityAccessToken.accessTokenTTL) === 0 - ? undefined - : { - expiresIn: Number(identityAccessToken.accessTokenTTL) - } - ); + if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) { + authAttemptCounter.add(1, { + "infisical.identity.id": identityGcpAuth.identityId, + "infisical.identity.name": identity.name, + "infisical.organization.id": org.id, + "infisical.organization.name": org.name, + "infisical.identity.auth_method": AuthAttemptAuthMethod.GCP_AUTH, + "infisical.identity.auth_result": AuthAttemptAuthResult.SUCCESS, + "client.address": requestContext.get("ip"), + "user_agent.original": requestContext.get("userAgent") + }); + } - return { accessToken, identityGcpAuth, identityAccessToken, identity }; + return { accessToken, identityGcpAuth, identityAccessToken, identity }; + } catch (error) { + if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) { + authAttemptCounter.add(1, { + "infisical.identity.id": identityGcpAuth.identityId, + "infisical.identity.name": identity.name, + "infisical.organization.id": org.id, + "infisical.organization.name": org.name, + "infisical.identity.auth_method": AuthAttemptAuthMethod.GCP_AUTH, + "infisical.identity.auth_result": AuthAttemptAuthResult.FAILURE, + "client.address": requestContext.get("ip"), + "user_agent.original": requestContext.get("userAgent") + }); + } + throw error; + } }; const attachGcpAuth = async ({ diff --git a/backend/src/services/identity-jwt-auth/identity-jwt-auth-service.ts b/backend/src/services/identity-jwt-auth/identity-jwt-auth-service.ts index debd90933..3cf93fd16 100644 --- a/backend/src/services/identity-jwt-auth/identity-jwt-auth-service.ts +++ b/backend/src/services/identity-jwt-auth/identity-jwt-auth-service.ts @@ -1,4 +1,5 @@ import { ForbiddenError } from "@casl/ability"; +import { requestContext } from "@fastify/request-context"; import https from "https"; import jwt from "jsonwebtoken"; import { JwksClient } from "jwks-rsa"; @@ -21,6 +22,7 @@ import { UnauthorizedError } from "@app/lib/errors"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; +import { AuthAttemptAuthMethod, AuthAttemptAuthResult, authAttemptCounter } from "@app/lib/telemetry/metrics"; import { getValueByDot } from "@app/lib/template/dot-access"; import { ActorType, AuthTokenType } from "../auth/auth-type"; @@ -67,6 +69,7 @@ export const identityJwtAuthServiceFactory = ({ orgDAL }: TIdentityJwtAuthServiceFactoryDep) => { const login = async ({ identityId, jwt: jwtValue }: TLoginJwtAuthDTO) => { + const appCfg = getConfig(); const identityJwtAuth = await identityJwtAuthDAL.findOne({ identityId }); if (!identityJwtAuth) { throw new NotFoundError({ message: "JWT auth method not found for identity, did you configure JWT auth?" }); @@ -75,176 +78,205 @@ export const identityJwtAuthServiceFactory = ({ const identity = await identityDAL.findById(identityJwtAuth.identityId); if (!identity) throw new UnauthorizedError({ message: "Identity not found" }); - const { decryptor: orgDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({ - type: KmsDataKey.Organization, - orgId: identity.orgId - }); - - const decodedToken = crypto.jwt().decode(jwtValue, { complete: true }); - if (!decodedToken) { - throw new UnauthorizedError({ - message: "Invalid JWT" + const org = await orgDAL.findById(identity.orgId); + try { + const { decryptor: orgDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: identity.orgId }); - } - let tokenData: Record = {}; - - if (identityJwtAuth.configurationType === JwtConfigurationType.JWKS) { - let client: JwksClient; - if (identityJwtAuth.jwksUrl.includes("https:")) { - const decryptedJwksCaCert = orgDataKeyDecryptor({ - cipherTextBlob: identityJwtAuth.encryptedJwksCaCert - }).toString(); - - const requestAgent = new https.Agent({ ca: decryptedJwksCaCert, rejectUnauthorized: !!decryptedJwksCaCert }); - client = new JwksClient({ - jwksUri: identityJwtAuth.jwksUrl, - requestAgent - }); - } else { - client = new JwksClient({ - jwksUri: identityJwtAuth.jwksUrl + const decodedToken = crypto.jwt().decode(jwtValue, { complete: true }); + if (!decodedToken) { + throw new UnauthorizedError({ + message: "Invalid JWT" }); } - const { kid } = decodedToken.header as { kid: string }; - const jwtSigningKey = await client.getSigningKey(kid); + let tokenData: Record = {}; - try { - tokenData = crypto.jwt().verify(jwtValue, jwtSigningKey.getPublicKey()) as Record; - } catch (error) { - if (error instanceof jwt.JsonWebTokenError) { - throw new UnauthorizedError({ - message: `Access denied: ${error.message}` + if (identityJwtAuth.configurationType === JwtConfigurationType.JWKS) { + let client: JwksClient; + if (identityJwtAuth.jwksUrl.includes("https:")) { + const decryptedJwksCaCert = orgDataKeyDecryptor({ + cipherTextBlob: identityJwtAuth.encryptedJwksCaCert + }).toString(); + + const requestAgent = new https.Agent({ ca: decryptedJwksCaCert, rejectUnauthorized: !!decryptedJwksCaCert }); + client = new JwksClient({ + jwksUri: identityJwtAuth.jwksUrl, + requestAgent + }); + } else { + client = new JwksClient({ + jwksUri: identityJwtAuth.jwksUrl }); } - throw error; - } - } else { - const decryptedPublicKeys = orgDataKeyDecryptor({ cipherTextBlob: identityJwtAuth.encryptedPublicKeys }) - .toString() - .split(","); + const { kid } = decodedToken.header as { kid: string }; + const jwtSigningKey = await client.getSigningKey(kid); - const errors: string[] = []; - let isMatchAnyKey = false; - for (const publicKey of decryptedPublicKeys) { try { - tokenData = crypto.jwt().verify(jwtValue, publicKey) as Record; - isMatchAnyKey = true; + tokenData = crypto.jwt().verify(jwtValue, jwtSigningKey.getPublicKey()) as Record; } catch (error) { if (error instanceof jwt.JsonWebTokenError) { - errors.push(error.message); + throw new UnauthorizedError({ + message: `Access denied: ${error.message}` + }); + } + + throw error; + } + } else { + const decryptedPublicKeys = orgDataKeyDecryptor({ cipherTextBlob: identityJwtAuth.encryptedPublicKeys }) + .toString() + .split(","); + + const errors: string[] = []; + let isMatchAnyKey = false; + for (const publicKey of decryptedPublicKeys) { + try { + tokenData = crypto.jwt().verify(jwtValue, publicKey) as Record; + isMatchAnyKey = true; + } catch (error) { + if (error instanceof jwt.JsonWebTokenError) { + errors.push(error.message); + } } } - } - if (!isMatchAnyKey) { - throw new UnauthorizedError({ - message: `Access denied: JWT verification failed with all keys. Errors - ${errors.join("; ")}` - }); - } - } - - if (identityJwtAuth.boundIssuer) { - if (tokenData.iss !== identityJwtAuth.boundIssuer) { - throw new ForbiddenRequestError({ - message: "Access denied: issuer mismatch" - }); - } - } - - if (identityJwtAuth.boundSubject) { - if (!tokenData.sub) { - throw new UnauthorizedError({ - message: "Access denied: token has no subject field" - }); - } - - if (!doesFieldValueMatchJwtPolicy(tokenData.sub, identityJwtAuth.boundSubject)) { - throw new ForbiddenRequestError({ - message: "Access denied: subject not allowed" - }); - } - } - - if (identityJwtAuth.boundAudiences) { - if (!tokenData.aud) { - throw new UnauthorizedError({ - message: "Access denied: token has no audience field" - }); - } - - if ( - !identityJwtAuth.boundAudiences - .split(", ") - .some((policyValue) => doesFieldValueMatchJwtPolicy(tokenData.aud, policyValue)) - ) { - throw new UnauthorizedError({ - message: "Access denied: token audience not allowed" - }); - } - } - - if (identityJwtAuth.boundClaims) { - Object.keys(identityJwtAuth.boundClaims).forEach((claimKey) => { - const claimValue = (identityJwtAuth.boundClaims as Record)[claimKey]; - const value = getValueByDot(tokenData, claimKey); - - if (!value) { + if (!isMatchAnyKey) { throw new UnauthorizedError({ - message: `Access denied: token has no ${claimKey} field` + message: `Access denied: JWT verification failed with all keys. Errors - ${errors.join("; ")}` + }); + } + } + + if (identityJwtAuth.boundIssuer) { + if (tokenData.iss !== identityJwtAuth.boundIssuer) { + throw new ForbiddenRequestError({ + message: "Access denied: issuer mismatch" + }); + } + } + + if (identityJwtAuth.boundSubject) { + if (!tokenData.sub) { + throw new UnauthorizedError({ + message: "Access denied: token has no subject field" }); } - // handle both single and multi-valued claims - if (!claimValue.split(", ").some((claimEntry) => doesFieldValueMatchJwtPolicy(value, claimEntry))) { - throw new UnauthorizedError({ - message: `Access denied: claim mismatch for field ${claimKey}` + if (!doesFieldValueMatchJwtPolicy(tokenData.sub, identityJwtAuth.boundSubject)) { + throw new ForbiddenRequestError({ + message: "Access denied: subject not allowed" }); } + } + + if (identityJwtAuth.boundAudiences) { + if (!tokenData.aud) { + throw new UnauthorizedError({ + message: "Access denied: token has no audience field" + }); + } + + if ( + !identityJwtAuth.boundAudiences + .split(", ") + .some((policyValue) => doesFieldValueMatchJwtPolicy(tokenData.aud, policyValue)) + ) { + throw new UnauthorizedError({ + message: "Access denied: token audience not allowed" + }); + } + } + + if (identityJwtAuth.boundClaims) { + Object.keys(identityJwtAuth.boundClaims).forEach((claimKey) => { + const claimValue = (identityJwtAuth.boundClaims as Record)[claimKey]; + const value = getValueByDot(tokenData, claimKey); + + if (!value) { + throw new UnauthorizedError({ + message: `Access denied: token has no ${claimKey} field` + }); + } + + // handle both single and multi-valued claims + if (!claimValue.split(", ").some((claimEntry) => doesFieldValueMatchJwtPolicy(value, claimEntry))) { + throw new UnauthorizedError({ + message: `Access denied: claim mismatch for field ${claimKey}` + }); + } + }); + } + + const identityAccessToken = await identityJwtAuthDAL.transaction(async (tx) => { + await membershipIdentityDAL.update( + { scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id }, + { lastLoginAuthMethod: IdentityAuthMethod.JWT_AUTH, lastLoginTime: new Date() }, + tx + ); + const newToken = await identityAccessTokenDAL.create( + { + identityId: identityJwtAuth.identityId, + isAccessTokenRevoked: false, + accessTokenTTL: identityJwtAuth.accessTokenTTL, + accessTokenMaxTTL: identityJwtAuth.accessTokenMaxTTL, + accessTokenNumUses: 0, + accessTokenNumUsesLimit: identityJwtAuth.accessTokenNumUsesLimit, + authMethod: IdentityAuthMethod.JWT_AUTH + }, + tx + ); + + return newToken; }); - } - const identityAccessToken = await identityJwtAuthDAL.transaction(async (tx) => { - await membershipIdentityDAL.update( - { scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id }, - { lastLoginAuthMethod: IdentityAuthMethod.JWT_AUTH, lastLoginTime: new Date() }, - tx - ); - const newToken = await identityAccessTokenDAL.create( + const accessToken = crypto.jwt().sign( { identityId: identityJwtAuth.identityId, - isAccessTokenRevoked: false, - accessTokenTTL: identityJwtAuth.accessTokenTTL, - accessTokenMaxTTL: identityJwtAuth.accessTokenMaxTTL, - accessTokenNumUses: 0, - accessTokenNumUsesLimit: identityJwtAuth.accessTokenNumUsesLimit, - authMethod: IdentityAuthMethod.JWT_AUTH - }, - tx + identityAccessTokenId: identityAccessToken.id, + authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN + } as TIdentityAccessTokenJwtPayload, + appCfg.AUTH_SECRET, + // akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error + Number(identityAccessToken.accessTokenTTL) === 0 + ? undefined + : { + expiresIn: Number(identityAccessToken.accessTokenTTL) + } ); - return newToken; - }); + if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) { + authAttemptCounter.add(1, { + "infisical.identity.id": identityJwtAuth.identityId, + "infisical.identity.name": identity.name, + "infisical.organization.id": org.id, + "infisical.organization.name": org.name, + "infisical.identity.auth_method": AuthAttemptAuthMethod.JWT_AUTH, + "infisical.identity.auth_result": AuthAttemptAuthResult.SUCCESS, + "client.address": requestContext.get("ip"), + "user_agent.original": requestContext.get("userAgent") + }); + } - const appCfg = getConfig(); - const accessToken = crypto.jwt().sign( - { - identityId: identityJwtAuth.identityId, - identityAccessTokenId: identityAccessToken.id, - authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN - } as TIdentityAccessTokenJwtPayload, - appCfg.AUTH_SECRET, - // akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error - Number(identityAccessToken.accessTokenTTL) === 0 - ? undefined - : { - expiresIn: Number(identityAccessToken.accessTokenTTL) - } - ); - - return { accessToken, identityJwtAuth, identityAccessToken, identity }; + return { accessToken, identityJwtAuth, identityAccessToken, identity }; + } catch (error) { + if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) { + authAttemptCounter.add(1, { + "infisical.identity.id": identityJwtAuth.identityId, + "infisical.identity.name": identity.name, + "infisical.organization.id": org.id, + "infisical.organization.name": org.name, + "infisical.identity.auth_method": AuthAttemptAuthMethod.JWT_AUTH, + "infisical.identity.auth_result": AuthAttemptAuthResult.FAILURE, + "client.address": requestContext.get("ip"), + "user_agent.original": requestContext.get("userAgent") + }); + } + throw error; + } }; const attachJwtAuth = async ({ diff --git a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts index 8a8fd5091..b633dc433 100644 --- a/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts +++ b/backend/src/services/identity-kubernetes-auth/identity-kubernetes-auth-service.ts @@ -1,4 +1,5 @@ import { ForbiddenError } from "@casl/ability"; +import { requestContext } from "@fastify/request-context"; import axios, { AxiosError } from "axios"; import https from "https"; import RE2 from "re2"; @@ -37,6 +38,7 @@ import { GatewayHttpProxyActions, GatewayProxyProtocol, withGatewayProxy } from import { withGatewayV2Proxy } from "@app/lib/gateway-v2/gateway-v2"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { logger } from "@app/lib/logger"; +import { AuthAttemptAuthMethod, AuthAttemptAuthResult, authAttemptCounter } from "@app/lib/telemetry/metrics"; import { ActorType, AuthTokenType } from "../auth/auth-type"; import { TIdentityDALFactory } from "../identity/identity-dal"; @@ -182,6 +184,7 @@ export const identityKubernetesAuthServiceFactory = ({ }; const login = async ({ identityId, jwt: serviceAccountJwt }: TLoginKubernetesAuthDTO) => { + const appCfg = getConfig(); const identityKubernetesAuth = await identityKubernetesAuthDAL.findOne({ identityId }); if (!identityKubernetesAuth) { throw new NotFoundError({ @@ -192,294 +195,328 @@ export const identityKubernetesAuthServiceFactory = ({ const identity = await identityDAL.findById(identityKubernetesAuth.identityId); if (!identity) throw new UnauthorizedError({ message: "Identity not found" }); - const { decryptor } = await kmsService.createCipherPairWithDataKey({ - type: KmsDataKey.Organization, - orgId: identity.orgId - }); + const org = await orgDAL.findById(identity.orgId); - let caCert = ""; - if (identityKubernetesAuth.encryptedKubernetesCaCertificate) { - caCert = decryptor({ cipherTextBlob: identityKubernetesAuth.encryptedKubernetesCaCertificate }).toString(); - } + try { + const { decryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: identity.orgId + }); - const tokenReviewCallbackRaw = async (host = identityKubernetesAuth.kubernetesHost, port?: number) => { - logger.info({ host, port }, "tokenReviewCallbackRaw: Processing kubernetes token review using raw API"); - - if (!host || !identityKubernetesAuth.kubernetesHost) { - throw new BadRequestError({ - message: "Kubernetes host is required when token review mode is set to API" - }); + let caCert = ""; + if (identityKubernetesAuth.encryptedKubernetesCaCertificate) { + caCert = decryptor({ cipherTextBlob: identityKubernetesAuth.encryptedKubernetesCaCertificate }).toString(); } - let tokenReviewerJwt = ""; - if (identityKubernetesAuth.encryptedKubernetesTokenReviewerJwt) { - tokenReviewerJwt = decryptor({ - cipherTextBlob: identityKubernetesAuth.encryptedKubernetesTokenReviewerJwt - }).toString(); - } else { - // if no token reviewer is provided means the incoming token has to act as reviewer - tokenReviewerJwt = serviceAccountJwt; - } + const tokenReviewCallbackRaw = async (host = identityKubernetesAuth.kubernetesHost, port?: number) => { + logger.info({ host, port }, "tokenReviewCallbackRaw: Processing kubernetes token review using raw API"); - let servername = identityKubernetesAuth.kubernetesHost; - if (servername.startsWith("https://") || servername.startsWith("http://")) { - servername = new RE2("^https?:\\/\\/").replace(servername, ""); - } + if (!host || !identityKubernetesAuth.kubernetesHost) { + throw new BadRequestError({ + message: "Kubernetes host is required when token review mode is set to API" + }); + } - // get the last colon index, if it has a port, remove it, including the colon - const lastColonIndex = servername.lastIndexOf(":"); - if (lastColonIndex !== -1) { - servername = servername.substring(0, lastColonIndex); - } + let tokenReviewerJwt = ""; + if (identityKubernetesAuth.encryptedKubernetesTokenReviewerJwt) { + tokenReviewerJwt = decryptor({ + cipherTextBlob: identityKubernetesAuth.encryptedKubernetesTokenReviewerJwt + }).toString(); + } else { + // if no token reviewer is provided means the incoming token has to act as reviewer + tokenReviewerJwt = serviceAccountJwt; + } - const baseUrl = port ? `${host}:${port}` : host; + let servername = identityKubernetesAuth.kubernetesHost; + if (servername.startsWith("https://") || servername.startsWith("http://")) { + servername = new RE2("^https?:\\/\\/").replace(servername, ""); + } - const res = await axios - .post( - `${baseUrl}/apis/authentication.k8s.io/v1/tokenreviews`, - { - apiVersion: "authentication.k8s.io/v1", - kind: "TokenReview", - spec: { - token: serviceAccountJwt, - ...(identityKubernetesAuth.allowedAudience ? { audiences: [identityKubernetesAuth.allowedAudience] } : {}) - } - }, - { - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${tokenReviewerJwt}` + // get the last colon index, if it has a port, remove it, including the colon + const lastColonIndex = servername.lastIndexOf(":"); + if (lastColonIndex !== -1) { + servername = servername.substring(0, lastColonIndex); + } + + const baseUrl = port ? `${host}:${port}` : host; + + const res = await axios + .post( + `${baseUrl}/apis/authentication.k8s.io/v1/tokenreviews`, + { + apiVersion: "authentication.k8s.io/v1", + kind: "TokenReview", + spec: { + token: serviceAccountJwt, + ...(identityKubernetesAuth.allowedAudience + ? { audiences: [identityKubernetesAuth.allowedAudience] } + : {}) + } }, - signal: AbortSignal.timeout(10000), - timeout: 10000, - httpsAgent: new https.Agent({ - ca: caCert, - rejectUnauthorized: Boolean(caCert), - servername - }) - } - ) - .catch((err) => { - if (err instanceof AxiosError) { - if (err.response) { - const { message } = err?.response?.data as unknown as { message?: string }; - - if (message) { - throw new UnauthorizedError({ - message, - name: "KubernetesTokenReviewRequestError" - }); - } - } - } - throw err; - }); - - return res.data; - }; - - const tokenReviewCallbackThroughGateway = async (host: string, port?: number) => { - logger.info( - { - host, - port - }, - "tokenReviewCallbackThroughGateway: Processing kubernetes token review using gateway" - ); - - const res = await axios - .post( - `${host}:${port}/apis/authentication.k8s.io/v1/tokenreviews`, - { - apiVersion: "authentication.k8s.io/v1", - kind: "TokenReview", - spec: { - token: serviceAccountJwt, - ...(identityKubernetesAuth.allowedAudience ? { audiences: [identityKubernetesAuth.allowedAudience] } : {}) - } - }, - { - headers: { - "Content-Type": "application/json", - "x-infisical-action": GatewayHttpProxyActions.UseGatewayK8sServiceAccount - }, - signal: AbortSignal.timeout(10000), - timeout: 10000 - } - ) - .catch((err) => { - if (err instanceof AxiosError) { - if (err.response) { - let { message } = err?.response?.data as unknown as { message?: string }; - - if (!message && typeof err.response.data === "string") { - message = err.response.data; - } - - if (message) { - throw new UnauthorizedError({ - message, - name: "KubernetesTokenReviewRequestError" - }); - } - } - } - throw err; - }); - - return res.data; - }; - - let data: TCreateTokenReviewResponse | undefined; - - if (identityKubernetesAuth.tokenReviewMode === IdentityKubernetesAuthTokenReviewMode.Gateway) { - if (!identityKubernetesAuth.gatewayId && !identityKubernetesAuth.gatewayV2Id) { - throw new BadRequestError({ - message: "Gateway ID is required when token review mode is set to Gateway" - }); - } - - data = await $gatewayProxyWrapper( - { - gatewayId: (identityKubernetesAuth.gatewayV2Id ?? identityKubernetesAuth.gatewayId) as string, - reviewTokenThroughGateway: true - }, - tokenReviewCallbackThroughGateway - ); - } else if (identityKubernetesAuth.tokenReviewMode === IdentityKubernetesAuthTokenReviewMode.Api) { - if (!identityKubernetesAuth.kubernetesHost) { - throw new BadRequestError({ - message: "Kubernetes host is required when token review mode is set to API" - }); - } - - let { kubernetesHost } = identityKubernetesAuth; - if (kubernetesHost.startsWith("https://") || kubernetesHost.startsWith("http://")) { - kubernetesHost = new RE2("^https?:\\/\\/").replace(kubernetesHost, ""); - } - - const [k8sHost, k8sPort] = kubernetesHost.split(":"); - - data = - identityKubernetesAuth.gatewayId || identityKubernetesAuth.gatewayV2Id - ? await $gatewayProxyWrapper( - { - gatewayId: (identityKubernetesAuth.gatewayV2Id ?? identityKubernetesAuth.gatewayId) as string, - targetHost: k8sHost, - targetPort: k8sPort ? Number(k8sPort) : 443, - reviewTokenThroughGateway: false + { + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${tokenReviewerJwt}` }, - tokenReviewCallbackRaw - ) - : await tokenReviewCallbackRaw(); - } else { - throw new BadRequestError({ - message: `Invalid token review mode: ${identityKubernetesAuth.tokenReviewMode}` - }); - } + signal: AbortSignal.timeout(10000), + timeout: 10000, + httpsAgent: new https.Agent({ + ca: caCert, + rejectUnauthorized: Boolean(caCert), + servername + }) + } + ) + .catch((err) => { + if (err instanceof AxiosError) { + if (err.response) { + const { message } = err?.response?.data as unknown as { message?: string }; - if (!data) { - throw new BadRequestError({ - message: "Failed to review token" - }); - } + if (message) { + throw new UnauthorizedError({ + message, + name: "KubernetesTokenReviewRequestError" + }); + } + } + } + throw err; + }); - if ("error" in data.status) - throw new UnauthorizedError({ message: data.status.error, name: "KubernetesTokenReviewError" }); + return res.data; + }; - // check the response to determine if the token is valid - if (!(data.status && data.status.authenticated)) - throw new UnauthorizedError({ - message: "Kubernetes token not authenticated", - name: "KubernetesTokenReviewError" + const tokenReviewCallbackThroughGateway = async (host: string, port?: number) => { + logger.info( + { + host, + port + }, + "tokenReviewCallbackThroughGateway: Processing kubernetes token review using gateway" + ); + + const res = await axios + .post( + `${host}:${port}/apis/authentication.k8s.io/v1/tokenreviews`, + { + apiVersion: "authentication.k8s.io/v1", + kind: "TokenReview", + spec: { + token: serviceAccountJwt, + ...(identityKubernetesAuth.allowedAudience + ? { audiences: [identityKubernetesAuth.allowedAudience] } + : {}) + } + }, + { + headers: { + "Content-Type": "application/json", + "x-infisical-action": GatewayHttpProxyActions.UseGatewayK8sServiceAccount + }, + signal: AbortSignal.timeout(10000), + timeout: 10000 + } + ) + .catch((err) => { + if (err instanceof AxiosError) { + if (err.response) { + let { message } = err?.response?.data as unknown as { message?: string }; + + if (!message && typeof err.response.data === "string") { + message = err.response.data; + } + + if (message) { + throw new UnauthorizedError({ + message, + name: "KubernetesTokenReviewRequestError" + }); + } + } + } + throw err; + }); + + return res.data; + }; + + let data: TCreateTokenReviewResponse | undefined; + + if (identityKubernetesAuth.tokenReviewMode === IdentityKubernetesAuthTokenReviewMode.Gateway) { + if (!identityKubernetesAuth.gatewayId && !identityKubernetesAuth.gatewayV2Id) { + throw new BadRequestError({ + message: "Gateway ID is required when token review mode is set to Gateway" + }); + } + + data = await $gatewayProxyWrapper( + { + gatewayId: (identityKubernetesAuth.gatewayV2Id ?? identityKubernetesAuth.gatewayId) as string, + reviewTokenThroughGateway: true + }, + tokenReviewCallbackThroughGateway + ); + } else if (identityKubernetesAuth.tokenReviewMode === IdentityKubernetesAuthTokenReviewMode.Api) { + if (!identityKubernetesAuth.kubernetesHost) { + throw new BadRequestError({ + message: "Kubernetes host is required when token review mode is set to API" + }); + } + + let { kubernetesHost } = identityKubernetesAuth; + if (kubernetesHost.startsWith("https://") || kubernetesHost.startsWith("http://")) { + kubernetesHost = new RE2("^https?:\\/\\/").replace(kubernetesHost, ""); + } + + const [k8sHost, k8sPort] = kubernetesHost.split(":"); + + data = + identityKubernetesAuth.gatewayId || identityKubernetesAuth.gatewayV2Id + ? await $gatewayProxyWrapper( + { + gatewayId: (identityKubernetesAuth.gatewayV2Id ?? identityKubernetesAuth.gatewayId) as string, + targetHost: k8sHost, + targetPort: k8sPort ? Number(k8sPort) : 443, + reviewTokenThroughGateway: false + }, + tokenReviewCallbackRaw + ) + : await tokenReviewCallbackRaw(); + } else { + throw new BadRequestError({ + message: `Invalid token review mode: ${identityKubernetesAuth.tokenReviewMode}` + }); + } + + if (!data) { + throw new BadRequestError({ + message: "Failed to review token" + }); + } + + if ("error" in data.status) + throw new UnauthorizedError({ message: data.status.error, name: "KubernetesTokenReviewError" }); + + // check the response to determine if the token is valid + if (!(data.status && data.status.authenticated)) + throw new UnauthorizedError({ + message: "Kubernetes token not authenticated", + name: "KubernetesTokenReviewError" + }); + + const { namespace: targetNamespace, name: targetName } = extractK8sUsername(data.status.user.username); + + if (identityKubernetesAuth.allowedNamespaces) { + // validate if [targetNamespace] is in the list of allowed namespaces + + const isNamespaceAllowed = identityKubernetesAuth.allowedNamespaces + .split(",") + .map((namespace) => namespace.trim()) + .some((namespace) => namespace === targetNamespace); + + if (!isNamespaceAllowed) + throw new UnauthorizedError({ + message: "Access denied: K8s namespace not allowed." + }); + } + + if (identityKubernetesAuth.allowedNames) { + // validate if [targetName] is in the list of allowed names + + const isNameAllowed = identityKubernetesAuth.allowedNames + .split(",") + .map((name) => name.trim()) + .some((name) => name === targetName); + + if (!isNameAllowed) + throw new UnauthorizedError({ + message: "Access denied: K8s name not allowed." + }); + } + + if (identityKubernetesAuth.allowedAudience) { + // validate if [audience] is in the list of allowed audiences + const isAudienceAllowed = data.status.audiences.some( + (audience) => audience === identityKubernetesAuth.allowedAudience + ); + + if (!isAudienceAllowed) + throw new UnauthorizedError({ + message: "Access denied: K8s audience not allowed." + }); + } + + const identityAccessToken = await identityKubernetesAuthDAL.transaction(async (tx) => { + await membershipIdentityDAL.update( + { scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id }, + { lastLoginAuthMethod: IdentityAuthMethod.KUBERNETES_AUTH, lastLoginTime: new Date() }, + tx + ); + const newToken = await identityAccessTokenDAL.create( + { + identityId: identityKubernetesAuth.identityId, + isAccessTokenRevoked: false, + accessTokenTTL: identityKubernetesAuth.accessTokenTTL, + accessTokenMaxTTL: identityKubernetesAuth.accessTokenMaxTTL, + accessTokenNumUses: 0, + accessTokenNumUsesLimit: identityKubernetesAuth.accessTokenNumUsesLimit, + authMethod: IdentityAuthMethod.KUBERNETES_AUTH + }, + tx + ); + return newToken; }); - const { namespace: targetNamespace, name: targetName } = extractK8sUsername(data.status.user.username); - - if (identityKubernetesAuth.allowedNamespaces) { - // validate if [targetNamespace] is in the list of allowed namespaces - - const isNamespaceAllowed = identityKubernetesAuth.allowedNamespaces - .split(",") - .map((namespace) => namespace.trim()) - .some((namespace) => namespace === targetNamespace); - - if (!isNamespaceAllowed) - throw new UnauthorizedError({ - message: "Access denied: K8s namespace not allowed." - }); - } - - if (identityKubernetesAuth.allowedNames) { - // validate if [targetName] is in the list of allowed names - - const isNameAllowed = identityKubernetesAuth.allowedNames - .split(",") - .map((name) => name.trim()) - .some((name) => name === targetName); - - if (!isNameAllowed) - throw new UnauthorizedError({ - message: "Access denied: K8s name not allowed." - }); - } - - if (identityKubernetesAuth.allowedAudience) { - // validate if [audience] is in the list of allowed audiences - const isAudienceAllowed = data.status.audiences.some( - (audience) => audience === identityKubernetesAuth.allowedAudience - ); - - if (!isAudienceAllowed) - throw new UnauthorizedError({ - message: "Access denied: K8s audience not allowed." - }); - } - - const identityAccessToken = await identityKubernetesAuthDAL.transaction(async (tx) => { - await membershipIdentityDAL.update( - { scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id }, - { lastLoginAuthMethod: IdentityAuthMethod.KUBERNETES_AUTH, lastLoginTime: new Date() }, - tx - ); - const newToken = await identityAccessTokenDAL.create( + const accessToken = crypto.jwt().sign( { identityId: identityKubernetesAuth.identityId, - isAccessTokenRevoked: false, - accessTokenTTL: identityKubernetesAuth.accessTokenTTL, - accessTokenMaxTTL: identityKubernetesAuth.accessTokenMaxTTL, - accessTokenNumUses: 0, - accessTokenNumUsesLimit: identityKubernetesAuth.accessTokenNumUsesLimit, - authMethod: IdentityAuthMethod.KUBERNETES_AUTH - }, - tx + identityAccessTokenId: identityAccessToken.id, + authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN, + identityAuth: { + kubernetes: { + namespace: targetNamespace, + name: targetName + } + } + } as TIdentityAccessTokenJwtPayload, + appCfg.AUTH_SECRET, + // akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error + Number(identityAccessToken.accessTokenTTL) === 0 + ? undefined + : { + expiresIn: Number(identityAccessToken.accessTokenTTL) + } ); - return newToken; - }); - const appCfg = getConfig(); - const accessToken = crypto.jwt().sign( - { - identityId: identityKubernetesAuth.identityId, - identityAccessTokenId: identityAccessToken.id, - authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN, - identityAuth: { - kubernetes: { - namespace: targetNamespace, - name: targetName - } - } - } as TIdentityAccessTokenJwtPayload, - appCfg.AUTH_SECRET, - // akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error - Number(identityAccessToken.accessTokenTTL) === 0 - ? undefined - : { - expiresIn: Number(identityAccessToken.accessTokenTTL) - } - ); + if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) { + authAttemptCounter.add(1, { + "infisical.identity.id": identityKubernetesAuth.identityId, + "infisical.identity.name": identity.name, + "infisical.organization.id": org.id, + "infisical.organization.name": org.name, + "infisical.identity.auth_method": AuthAttemptAuthMethod.KUBERNETES_AUTH, + "infisical.identity.auth_result": AuthAttemptAuthResult.SUCCESS, + "client.address": requestContext.get("ip"), + "user_agent.original": requestContext.get("userAgent") + }); + } - return { accessToken, identityKubernetesAuth, identityAccessToken, identity }; + return { accessToken, identityKubernetesAuth, identityAccessToken, identity }; + } catch (error) { + if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) { + authAttemptCounter.add(1, { + "infisical.identity.id": identityKubernetesAuth.identityId, + "infisical.identity.name": identity.name, + "infisical.organization.id": org.id, + "infisical.organization.name": org.name, + "infisical.identity.auth_method": AuthAttemptAuthMethod.KUBERNETES_AUTH, + "infisical.identity.auth_result": AuthAttemptAuthResult.FAILURE, + "client.address": requestContext.get("ip"), + "user_agent.original": requestContext.get("userAgent") + }); + } + throw error; + } }; const attachKubernetesAuth = async ({ diff --git a/backend/src/services/identity-ldap-auth/identity-ldap-auth-service.ts b/backend/src/services/identity-ldap-auth/identity-ldap-auth-service.ts index 272e45c4e..d327dabee 100644 --- a/backend/src/services/identity-ldap-auth/identity-ldap-auth-service.ts +++ b/backend/src/services/identity-ldap-auth/identity-ldap-auth-service.ts @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { ForbiddenError } from "@casl/ability"; +import { requestContext } from "@fastify/request-context"; import slugify from "@sindresorhus/slugify"; import { AccessScope, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas"; @@ -29,6 +30,7 @@ import { } from "@app/lib/errors"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { logger } from "@app/lib/logger"; +import { AuthAttemptAuthMethod, AuthAttemptAuthResult, authAttemptCounter } from "@app/lib/telemetry/metrics"; import { ActorType, AuthTokenType } from "../auth/auth-type"; import { TIdentityDALFactory } from "../identity/identity-dal"; @@ -151,6 +153,7 @@ export const identityLdapAuthServiceFactory = ({ }; const login = async ({ identityId }: TLoginLdapAuthDTO) => { + const appCfg = getConfig(); const identityLdapAuth = await identityLdapAuthDAL.findOne({ identityId }); if (!identityLdapAuth) { @@ -162,6 +165,7 @@ export const identityLdapAuthServiceFactory = ({ const identity = await identityDAL.findById(identityLdapAuth.identityId); if (!identity) throw new UnauthorizedError({ message: "Identity not found" }); + const org = await orgDAL.findById(identity.orgId); const plan = await licenseService.getPlan(identity.orgId); if (!plan.ldap) { throw new BadRequestError({ @@ -170,44 +174,72 @@ export const identityLdapAuthServiceFactory = ({ }); } - const identityAccessToken = await identityLdapAuthDAL.transaction(async (tx) => { - await membershipIdentityDAL.update( - { scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id }, - { lastLoginAuthMethod: IdentityAuthMethod.LDAP_AUTH, lastLoginTime: new Date() }, - tx - ); - const newToken = await identityAccessTokenDAL.create( + try { + const identityAccessToken = await identityLdapAuthDAL.transaction(async (tx) => { + await membershipIdentityDAL.update( + { scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id }, + { lastLoginAuthMethod: IdentityAuthMethod.LDAP_AUTH, lastLoginTime: new Date() }, + tx + ); + const newToken = await identityAccessTokenDAL.create( + { + identityId: identityLdapAuth.identityId, + isAccessTokenRevoked: false, + accessTokenTTL: identityLdapAuth.accessTokenTTL, + accessTokenMaxTTL: identityLdapAuth.accessTokenMaxTTL, + accessTokenNumUses: 0, + accessTokenNumUsesLimit: identityLdapAuth.accessTokenNumUsesLimit, + authMethod: IdentityAuthMethod.LDAP_AUTH + }, + tx + ); + return newToken; + }); + + const accessToken = crypto.jwt().sign( { identityId: identityLdapAuth.identityId, - isAccessTokenRevoked: false, - accessTokenTTL: identityLdapAuth.accessTokenTTL, - accessTokenMaxTTL: identityLdapAuth.accessTokenMaxTTL, - accessTokenNumUses: 0, - accessTokenNumUsesLimit: identityLdapAuth.accessTokenNumUsesLimit, - authMethod: IdentityAuthMethod.LDAP_AUTH - }, - tx + identityAccessTokenId: identityAccessToken.id, + authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN + } as TIdentityAccessTokenJwtPayload, + appCfg.AUTH_SECRET, + // akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error + Number(identityAccessToken.accessTokenTTL) === 0 + ? undefined + : { + expiresIn: Number(identityAccessToken.accessTokenTTL) + } ); - return newToken; - }); - const appCfg = getConfig(); - const accessToken = crypto.jwt().sign( - { - identityId: identityLdapAuth.identityId, - identityAccessTokenId: identityAccessToken.id, - authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN - } as TIdentityAccessTokenJwtPayload, - appCfg.AUTH_SECRET, - // akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error - Number(identityAccessToken.accessTokenTTL) === 0 - ? undefined - : { - expiresIn: Number(identityAccessToken.accessTokenTTL) - } - ); + if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) { + authAttemptCounter.add(1, { + "infisical.identity.id": identityLdapAuth.identityId, + "infisical.identity.name": identity.name, + "infisical.organization.id": org.id, + "infisical.organization.name": org.name, + "infisical.identity.auth_method": AuthAttemptAuthMethod.LDAP_AUTH, + "infisical.identity.auth_result": AuthAttemptAuthResult.SUCCESS, + "client.address": requestContext.get("ip"), + "user_agent.original": requestContext.get("userAgent") + }); + } - return { accessToken, identityLdapAuth, identityAccessToken, identity }; + return { accessToken, identityLdapAuth, identityAccessToken, identity }; + } catch (error) { + if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) { + authAttemptCounter.add(1, { + "infisical.identity.id": identityLdapAuth.identityId, + "infisical.identity.name": identity.name, + "infisical.organization.id": org.id, + "infisical.organization.name": org.name, + "infisical.identity.auth_method": AuthAttemptAuthMethod.LDAP_AUTH, + "infisical.identity.auth_result": AuthAttemptAuthResult.FAILURE, + "client.address": requestContext.get("ip"), + "user_agent.original": requestContext.get("userAgent") + }); + } + throw error; + } }; const attachLdapAuth = async ({ diff --git a/backend/src/services/identity-oci-auth/identity-oci-auth-service.ts b/backend/src/services/identity-oci-auth/identity-oci-auth-service.ts index 6d7f0c4d3..c75abc76b 100644 --- a/backend/src/services/identity-oci-auth/identity-oci-auth-service.ts +++ b/backend/src/services/identity-oci-auth/identity-oci-auth-service.ts @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { ForbiddenError } from "@casl/ability"; +import { requestContext } from "@fastify/request-context"; import { AxiosError } from "axios"; import RE2 from "re2"; @@ -23,6 +24,7 @@ import { } from "@app/lib/errors"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; import { logger } from "@app/lib/logger"; +import { AuthAttemptAuthMethod, AuthAttemptAuthResult, authAttemptCounter } from "@app/lib/telemetry/metrics"; import { ActorType, AuthTokenType } from "../auth/auth-type"; import { TIdentityDALFactory } from "../identity/identity-dal"; @@ -63,6 +65,7 @@ export const identityOciAuthServiceFactory = ({ orgDAL }: TIdentityOciAuthServiceFactoryDep) => { const login = async ({ identityId, headers, userOcid }: TLoginOciAuthDTO) => { + const appCfg = getConfig(); const identityOciAuth = await identityOciAuthDAL.findOne({ identityId }); if (!identityOciAuth) { throw new NotFoundError({ message: "OCI auth method not found for identity, did you configure OCI auth?" }); @@ -71,80 +74,109 @@ export const identityOciAuthServiceFactory = ({ const identity = await identityDAL.findById(identityOciAuth.identityId); if (!identity) throw new UnauthorizedError({ message: "Identity not found" }); - // Validate OCI host format. Ensures that the host is in "identity..oraclecloud.com" format. - if (!headers.host || !new RE2("^identity\\.([a-z]{2}-[a-z]+-[1-9])\\.oraclecloud\\.com$").test(headers.host)) { - throw new BadRequestError({ - message: "Invalid OCI host format. Expected format: identity..oraclecloud.com" - }); - } - - const { data } = await request - .get(`https://${headers.host}/20160918/users/${userOcid}`, { - headers - }) - .catch((err: AxiosError) => { - logger.error(err.response, "OciIdentityLogin: Failed to authenticate with Oracle Cloud"); - throw err; - }); - - if (data.compartmentId !== identityOciAuth.tenancyOcid) { - throw new UnauthorizedError({ - message: "Access denied: OCI account isn't part of tenancy." - }); - } - - if (identityOciAuth.allowedUsernames) { - const isAccountAllowed = identityOciAuth.allowedUsernames.split(",").some((name) => name.trim() === data.name); - - if (!isAccountAllowed) - throw new UnauthorizedError({ - message: "Access denied: OCI account username not allowed." + const org = await orgDAL.findById(identity.orgId); + try { + // Validate OCI host format. Ensures that the host is in "identity..oraclecloud.com" format. + if (!headers.host || !new RE2("^identity\\.([a-z]{2}-[a-z]+-[1-9])\\.oraclecloud\\.com$").test(headers.host)) { + throw new BadRequestError({ + message: "Invalid OCI host format. Expected format: identity..oraclecloud.com" }); - } + } - // Generate the token - const identityAccessToken = await identityOciAuthDAL.transaction(async (tx) => { - await membershipIdentityDAL.update( - { scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id }, - { lastLoginAuthMethod: IdentityAuthMethod.OCI_AUTH, lastLoginTime: new Date() }, - tx - ); - const newToken = await identityAccessTokenDAL.create( + const { data } = await request + .get(`https://${headers.host}/20160918/users/${userOcid}`, { + headers + }) + .catch((err: AxiosError) => { + logger.error(err.response, "OciIdentityLogin: Failed to authenticate with Oracle Cloud"); + throw err; + }); + + if (data.compartmentId !== identityOciAuth.tenancyOcid) { + throw new UnauthorizedError({ + message: "Access denied: OCI account isn't part of tenancy." + }); + } + + if (identityOciAuth.allowedUsernames) { + const isAccountAllowed = identityOciAuth.allowedUsernames.split(",").some((name) => name.trim() === data.name); + + if (!isAccountAllowed) + throw new UnauthorizedError({ + message: "Access denied: OCI account username not allowed." + }); + } + + // Generate the token + const identityAccessToken = await identityOciAuthDAL.transaction(async (tx) => { + await membershipIdentityDAL.update( + { scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id }, + { lastLoginAuthMethod: IdentityAuthMethod.OCI_AUTH, lastLoginTime: new Date() }, + tx + ); + const newToken = await identityAccessTokenDAL.create( + { + identityId: identityOciAuth.identityId, + isAccessTokenRevoked: false, + accessTokenTTL: identityOciAuth.accessTokenTTL, + accessTokenMaxTTL: identityOciAuth.accessTokenMaxTTL, + accessTokenNumUses: 0, + accessTokenNumUsesLimit: identityOciAuth.accessTokenNumUsesLimit, + authMethod: IdentityAuthMethod.OCI_AUTH + }, + tx + ); + return newToken; + }); + + const accessToken = crypto.jwt().sign( { identityId: identityOciAuth.identityId, - isAccessTokenRevoked: false, - accessTokenTTL: identityOciAuth.accessTokenTTL, - accessTokenMaxTTL: identityOciAuth.accessTokenMaxTTL, - accessTokenNumUses: 0, - accessTokenNumUsesLimit: identityOciAuth.accessTokenNumUsesLimit, - authMethod: IdentityAuthMethod.OCI_AUTH - }, - tx + identityAccessTokenId: identityAccessToken.id, + authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN + } as TIdentityAccessTokenJwtPayload, + appCfg.AUTH_SECRET, + Number(identityAccessToken.accessTokenTTL) === 0 + ? undefined + : { + expiresIn: Number(identityAccessToken.accessTokenTTL) + } ); - return newToken; - }); - const appCfg = getConfig(); - const accessToken = crypto.jwt().sign( - { - identityId: identityOciAuth.identityId, - identityAccessTokenId: identityAccessToken.id, - authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN - } as TIdentityAccessTokenJwtPayload, - appCfg.AUTH_SECRET, - Number(identityAccessToken.accessTokenTTL) === 0 - ? undefined - : { - expiresIn: Number(identityAccessToken.accessTokenTTL) - } - ); + if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) { + authAttemptCounter.add(1, { + "infisical.identity.id": identityOciAuth.identityId, + "infisical.identity.name": identity.name, + "infisical.organization.id": org.id, + "infisical.organization.name": org.name, + "infisical.identity.auth_method": AuthAttemptAuthMethod.OCI_AUTH, + "infisical.identity.auth_result": AuthAttemptAuthResult.SUCCESS, + "client.address": requestContext.get("ip"), + "user_agent.original": requestContext.get("userAgent") + }); + } - return { - identityOciAuth, - accessToken, - identityAccessToken, - identity - }; + return { + identityOciAuth, + accessToken, + identityAccessToken, + identity + }; + } catch (error) { + if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) { + authAttemptCounter.add(1, { + "infisical.identity.id": identityOciAuth.identityId, + "infisical.identity.name": identity.name, + "infisical.organization.id": org.id, + "infisical.organization.name": org.name, + "infisical.identity.auth_method": AuthAttemptAuthMethod.OCI_AUTH, + "infisical.identity.auth_result": AuthAttemptAuthResult.FAILURE, + "client.address": requestContext.get("ip"), + "user_agent.original": requestContext.get("userAgent") + }); + } + throw error; + } }; const attachOciAuth = async ({ diff --git a/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts b/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts index 628b69f14..a03beeb3b 100644 --- a/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts +++ b/backend/src/services/identity-oidc-auth/identity-oidc-auth-service.ts @@ -1,4 +1,5 @@ import { ForbiddenError } from "@casl/ability"; +import { requestContext } from "@fastify/request-context"; import axios from "axios"; import https from "https"; import jwt from "jsonwebtoken"; @@ -22,6 +23,7 @@ import { UnauthorizedError } from "@app/lib/errors"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; +import { AuthAttemptAuthMethod, AuthAttemptAuthResult, authAttemptCounter } from "@app/lib/telemetry/metrics"; import { getValueByDot } from "@app/lib/template/dot-access"; import { ActorType, AuthTokenType } from "../auth/auth-type"; @@ -67,6 +69,7 @@ export const identityOidcAuthServiceFactory = ({ orgDAL }: TIdentityOidcAuthServiceFactoryDep) => { const login = async ({ identityId, jwt: oidcJwt }: TLoginOidcAuthDTO) => { + const appCfg = getConfig(); const identityOidcAuth = await identityOidcAuthDAL.findOne({ identityId }); if (!identityOidcAuth) { throw new NotFoundError({ message: "OIDC auth method not found for identity, did you configure OIDC auth?" }); @@ -75,151 +78,180 @@ export const identityOidcAuthServiceFactory = ({ const identity = await identityDAL.findById(identityOidcAuth.identityId); if (!identity) throw new UnauthorizedError({ message: "Identity not found" }); - const { decryptor } = await kmsService.createCipherPairWithDataKey({ - type: KmsDataKey.Organization, - orgId: identity.orgId - }); - - let caCert = ""; - if (identityOidcAuth.encryptedCaCertificate) { - caCert = decryptor({ cipherTextBlob: identityOidcAuth.encryptedCaCertificate }).toString(); - } - - const requestAgent = new https.Agent({ ca: caCert, rejectUnauthorized: !!caCert }); - const { data: discoveryDoc } = await axios.get<{ jwks_uri: string }>( - `${identityOidcAuth.oidcDiscoveryUrl}/.well-known/openid-configuration`, - { - httpsAgent: identityOidcAuth.oidcDiscoveryUrl.includes("https") ? requestAgent : undefined - } - ); - const jwksUri = discoveryDoc.jwks_uri; - - const decodedToken = crypto.jwt().decode(oidcJwt, { complete: true }); - if (!decodedToken) { - throw new UnauthorizedError({ - message: "Invalid JWT" - }); - } - - const client = new JwksClient({ - jwksUri, - requestAgent: identityOidcAuth.oidcDiscoveryUrl.includes("https") ? requestAgent : undefined - }); - - const { kid } = decodedToken.header as { kid: string }; - const oidcSigningKey = await client.getSigningKey(kid); - - let tokenData: Record; + const org = await orgDAL.findById(identity.orgId); try { - tokenData = crypto.jwt().verify(oidcJwt, oidcSigningKey.getPublicKey(), { - issuer: identityOidcAuth.boundIssuer - }) as Record; - } catch (error) { - if (error instanceof jwt.JsonWebTokenError) { + const { decryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: identity.orgId + }); + + let caCert = ""; + if (identityOidcAuth.encryptedCaCertificate) { + caCert = decryptor({ cipherTextBlob: identityOidcAuth.encryptedCaCertificate }).toString(); + } + + const requestAgent = new https.Agent({ ca: caCert, rejectUnauthorized: !!caCert }); + const { data: discoveryDoc } = await axios.get<{ jwks_uri: string }>( + `${identityOidcAuth.oidcDiscoveryUrl}/.well-known/openid-configuration`, + { + httpsAgent: identityOidcAuth.oidcDiscoveryUrl.includes("https") ? requestAgent : undefined + } + ); + const jwksUri = discoveryDoc.jwks_uri; + + const decodedToken = crypto.jwt().decode(oidcJwt, { complete: true }); + if (!decodedToken) { throw new UnauthorizedError({ - message: `Access denied: ${error.message}` + message: "Invalid JWT" + }); + } + + const client = new JwksClient({ + jwksUri, + requestAgent: identityOidcAuth.oidcDiscoveryUrl.includes("https") ? requestAgent : undefined + }); + + const { kid } = decodedToken.header as { kid: string }; + const oidcSigningKey = await client.getSigningKey(kid); + + let tokenData: Record; + try { + tokenData = crypto.jwt().verify(oidcJwt, oidcSigningKey.getPublicKey(), { + issuer: identityOidcAuth.boundIssuer + }) as Record; + } 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)[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 = {}; + if (identityOidcAuth.claimMetadataMapping) { + Object.keys(identityOidcAuth.claimMetadataMapping).forEach((permissionKey) => { + const claimKey = (identityOidcAuth.claimMetadataMapping as Record)[permissionKey]; + const value = getValueByDot(tokenData, claimKey); + if (!value) { + throw new UnauthorizedError({ + message: `Access denied: token has no ${claimKey} field` + }); + } + filteredClaims[permissionKey] = value.toString(); + }); + } + + const identityAccessToken = await identityOidcAuthDAL.transaction(async (tx) => { + await membershipIdentityDAL.update( + { scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id }, + { lastLoginAuthMethod: IdentityAuthMethod.OIDC_AUTH, lastLoginTime: new Date() }, + tx + ); + const newToken = await identityAccessTokenDAL.create( + { + identityId: identityOidcAuth.identityId, + isAccessTokenRevoked: false, + accessTokenTTL: identityOidcAuth.accessTokenTTL, + accessTokenMaxTTL: identityOidcAuth.accessTokenMaxTTL, + accessTokenNumUses: 0, + accessTokenNumUsesLimit: identityOidcAuth.accessTokenNumUsesLimit, + authMethod: IdentityAuthMethod.OIDC_AUTH + }, + tx + ); + return newToken; + }); + + const accessToken = crypto.jwt().sign( + { + identityId: identityOidcAuth.identityId, + identityAccessTokenId: identityAccessToken.id, + authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN, + identityAuth: { + oidc: { + claims: filteredClaims + } + } + } as TIdentityAccessTokenJwtPayload, + appCfg.AUTH_SECRET, + // akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error + Number(identityAccessToken.accessTokenTTL) === 0 + ? undefined + : { + expiresIn: Number(identityAccessToken.accessTokenTTL) + } + ); + + if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) { + authAttemptCounter.add(1, { + "infisical.identity.id": identityOidcAuth.identityId, + "infisical.identity.name": identity.name, + "infisical.organization.id": org.id, + "infisical.organization.name": org.name, + "infisical.identity.auth_method": AuthAttemptAuthMethod.OIDC_AUTH, + "infisical.identity.auth_result": AuthAttemptAuthResult.SUCCESS, + "client.address": requestContext.get("ip"), + "user_agent.original": requestContext.get("userAgent") + }); + } + + return { accessToken, identityOidcAuth, identityAccessToken, identity, oidcTokenData: tokenData }; + } catch (error) { + if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) { + authAttemptCounter.add(1, { + "infisical.identity.id": identityOidcAuth.identityId, + "infisical.identity.name": identity.name, + "infisical.organization.id": org.id, + "infisical.organization.name": org.name, + "infisical.identity.auth_method": AuthAttemptAuthMethod.OIDC_AUTH, + "infisical.identity.auth_result": AuthAttemptAuthResult.FAILURE, + "client.address": requestContext.get("ip"), + "user_agent.original": requestContext.get("userAgent") }); } throw error; } - - if (identityOidcAuth.boundSubject) { - if (!doesFieldValueMatchOidcPolicy(tokenData.sub, identityOidcAuth.boundSubject)) { - throw new ForbiddenRequestError({ - message: "Access denied: OIDC subject not allowed." - }); - } - } - - if (identityOidcAuth.boundAudiences) { - if ( - !identityOidcAuth.boundAudiences - .split(", ") - .some((policyValue) => doesAudValueMatchOidcPolicy(tokenData.aud, policyValue)) - ) { - throw new UnauthorizedError({ - message: "Access denied: OIDC audience not allowed." - }); - } - } - - if (identityOidcAuth.boundClaims) { - Object.keys(identityOidcAuth.boundClaims).forEach((claimKey) => { - const claimValue = (identityOidcAuth.boundClaims as Record)[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 = {}; - if (identityOidcAuth.claimMetadataMapping) { - Object.keys(identityOidcAuth.claimMetadataMapping).forEach((permissionKey) => { - const claimKey = (identityOidcAuth.claimMetadataMapping as Record)[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 ({ diff --git a/backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-service.ts b/backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-service.ts index 24c82ccac..60670d035 100644 --- a/backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-service.ts +++ b/backend/src/services/identity-tls-cert-auth/identity-tls-cert-auth-service.ts @@ -1,4 +1,5 @@ import { ForbiddenError } from "@casl/ability"; +import { requestContext } from "@fastify/request-context"; import { AccessScope, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; @@ -19,6 +20,7 @@ import { UnauthorizedError } from "@app/lib/errors"; import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip"; +import { AuthAttemptAuthMethod, AuthAttemptAuthResult, authAttemptCounter } from "@app/lib/telemetry/metrics"; import { ActorType, AuthTokenType } from "../auth/auth-type"; import { TIdentityDALFactory } from "../identity/identity-dal"; @@ -27,6 +29,7 @@ import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identit import { TKmsServiceFactory } from "../kms/kms-service"; import { KmsDataKey } from "../kms/kms-types"; import { TMembershipIdentityDALFactory } from "../membership-identity/membership-identity-dal"; +import { TOrgDALFactory } from "../org/org-dal"; import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns"; import { TIdentityTlsCertAuthDALFactory } from "./identity-tls-cert-auth-dal"; import { TIdentityTlsCertAuthServiceFactory } from "./identity-tls-cert-auth-types"; @@ -42,6 +45,7 @@ type TIdentityTlsCertAuthServiceFactoryDep = { licenseService: Pick; permissionService: Pick; kmsService: Pick; + orgDAL: Pick; }; const parseSubjectDetails = (data: string) => { @@ -60,9 +64,11 @@ export const identityTlsCertAuthServiceFactory = ({ membershipIdentityDAL, licenseService, permissionService, - kmsService + kmsService, + orgDAL }: TIdentityTlsCertAuthServiceFactoryDep): TIdentityTlsCertAuthServiceFactory => { const login: TIdentityTlsCertAuthServiceFactory["login"] = async ({ identityId, clientCertificate }) => { + const appCfg = getConfig(); const identityTlsCertAuth = await identityTlsCertAuthDAL.findOne({ identityId }); if (!identityTlsCertAuth) { throw new NotFoundError({ @@ -73,94 +79,124 @@ export const identityTlsCertAuthServiceFactory = ({ const identity = await identityDAL.findById(identityTlsCertAuth.identityId); if (!identity) throw new UnauthorizedError({ message: "Identity not found" }); - const { decryptor } = await kmsService.createCipherPairWithDataKey({ - type: KmsDataKey.Organization, - orgId: identity.orgId - }); + const org = await orgDAL.findById(identity.orgId); - const caCertificate = decryptor({ - cipherTextBlob: identityTlsCertAuth.encryptedCaCertificate - }).toString(); - - const leafCertificate = extractX509CertFromChain(decodeURIComponent(clientCertificate))?.[0]; - if (!leafCertificate) { - throw new BadRequestError({ message: "Missing client certificate" }); - } - - const clientCertificateX509 = new crypto.nativeCrypto.X509Certificate(leafCertificate); - const caCertificateX509 = new crypto.nativeCrypto.X509Certificate(caCertificate); - - const isValidCertificate = clientCertificateX509.verify(caCertificateX509.publicKey); - if (!isValidCertificate) - throw new UnauthorizedError({ - message: "Access denied: Certificate not issued by the provided CA." + try { + const { decryptor } = await kmsService.createCipherPairWithDataKey({ + type: KmsDataKey.Organization, + orgId: identity.orgId }); - if (new Date(clientCertificateX509.validTo) < new Date()) { - throw new UnauthorizedError({ - message: "Access denied: Certificate has expired." - }); - } + const caCertificate = decryptor({ + cipherTextBlob: identityTlsCertAuth.encryptedCaCertificate + }).toString(); - if (new Date(clientCertificateX509.validFrom) > new Date()) { - throw new UnauthorizedError({ - message: "Access denied: Certificate not yet valid." - }); - } + const leafCertificate = extractX509CertFromChain(decodeURIComponent(clientCertificate))?.[0]; + if (!leafCertificate) { + throw new BadRequestError({ message: "Missing client certificate" }); + } - const subjectDetails = parseSubjectDetails(clientCertificateX509.subject); - if (identityTlsCertAuth.allowedCommonNames) { - const isValidCommonName = identityTlsCertAuth.allowedCommonNames.split(",").includes(subjectDetails.CN); - if (!isValidCommonName) { + const clientCertificateX509 = new crypto.nativeCrypto.X509Certificate(leafCertificate); + const caCertificateX509 = new crypto.nativeCrypto.X509Certificate(caCertificate); + + const isValidCertificate = clientCertificateX509.verify(caCertificateX509.publicKey); + if (!isValidCertificate) throw new UnauthorizedError({ - message: "Access denied: TLS Certificate Auth common name not allowed." + message: "Access denied: Certificate not issued by the provided CA." + }); + + if (new Date(clientCertificateX509.validTo) < new Date()) { + throw new UnauthorizedError({ + message: "Access denied: Certificate has expired." }); } - } - // Generate the token - const identityAccessToken = await identityTlsCertAuthDAL.transaction(async (tx) => { - await membershipIdentityDAL.update( - { scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id }, - { lastLoginAuthMethod: IdentityAuthMethod.TLS_CERT_AUTH, lastLoginTime: new Date() }, - tx - ); - const newToken = await identityAccessTokenDAL.create( + if (new Date(clientCertificateX509.validFrom) > new Date()) { + throw new UnauthorizedError({ + message: "Access denied: Certificate not yet valid." + }); + } + + const subjectDetails = parseSubjectDetails(clientCertificateX509.subject); + if (identityTlsCertAuth.allowedCommonNames) { + const isValidCommonName = identityTlsCertAuth.allowedCommonNames.split(",").includes(subjectDetails.CN); + if (!isValidCommonName) { + throw new UnauthorizedError({ + message: "Access denied: TLS Certificate Auth common name not allowed." + }); + } + } + + // Generate the token + const identityAccessToken = await identityTlsCertAuthDAL.transaction(async (tx) => { + await membershipIdentityDAL.update( + { scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id }, + { lastLoginAuthMethod: IdentityAuthMethod.TLS_CERT_AUTH, lastLoginTime: new Date() }, + tx + ); + const newToken = await identityAccessTokenDAL.create( + { + identityId: identityTlsCertAuth.identityId, + isAccessTokenRevoked: false, + accessTokenTTL: identityTlsCertAuth.accessTokenTTL, + accessTokenMaxTTL: identityTlsCertAuth.accessTokenMaxTTL, + accessTokenNumUses: 0, + accessTokenNumUsesLimit: identityTlsCertAuth.accessTokenNumUsesLimit, + authMethod: IdentityAuthMethod.TLS_CERT_AUTH + }, + tx + ); + return newToken; + }); + + const accessToken = crypto.jwt().sign( { identityId: identityTlsCertAuth.identityId, - isAccessTokenRevoked: false, - accessTokenTTL: identityTlsCertAuth.accessTokenTTL, - accessTokenMaxTTL: identityTlsCertAuth.accessTokenMaxTTL, - accessTokenNumUses: 0, - accessTokenNumUsesLimit: identityTlsCertAuth.accessTokenNumUsesLimit, - authMethod: IdentityAuthMethod.TLS_CERT_AUTH - }, - tx + identityAccessTokenId: identityAccessToken.id, + authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN + } as TIdentityAccessTokenJwtPayload, + appCfg.AUTH_SECRET, + Number(identityAccessToken.accessTokenTTL) === 0 + ? undefined + : { + expiresIn: Number(identityAccessToken.accessTokenTTL) + } ); - return newToken; - }); - const appCfg = getConfig(); - const accessToken = crypto.jwt().sign( - { - identityId: identityTlsCertAuth.identityId, - identityAccessTokenId: identityAccessToken.id, - authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN - } as TIdentityAccessTokenJwtPayload, - appCfg.AUTH_SECRET, - Number(identityAccessToken.accessTokenTTL) === 0 - ? undefined - : { - expiresIn: Number(identityAccessToken.accessTokenTTL) - } - ); + if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) { + authAttemptCounter.add(1, { + "infisical.identity.id": identityTlsCertAuth.identityId, + "infisical.identity.name": identity.name, + "infisical.organization.id": org.id, + "infisical.organization.name": org.name, + "infisical.identity.auth_method": AuthAttemptAuthMethod.TLS_CERT_AUTH, + "infisical.identity.auth_result": AuthAttemptAuthResult.SUCCESS, + "client.address": requestContext.get("ip"), + "user_agent.original": requestContext.get("userAgent") + }); + } - return { - identityTlsCertAuth, - accessToken, - identityAccessToken, - identity - }; + return { + identityTlsCertAuth, + accessToken, + identityAccessToken, + identity + }; + } catch (error) { + if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) { + authAttemptCounter.add(1, { + "infisical.identity.id": identityTlsCertAuth.identityId, + "infisical.identity.name": identity.name, + "infisical.organization.id": org.id, + "infisical.organization.name": org.name, + "infisical.identity.auth_method": AuthAttemptAuthMethod.TLS_CERT_AUTH, + "infisical.identity.auth_result": AuthAttemptAuthResult.FAILURE, + "client.address": requestContext.get("ip"), + "user_agent.original": requestContext.get("userAgent") + }); + } + throw error; + } }; const attachTlsCertAuth: TIdentityTlsCertAuthServiceFactory["attachTlsCertAuth"] = async ({ diff --git a/backend/src/services/identity-ua/identity-ua-service.ts b/backend/src/services/identity-ua/identity-ua-service.ts index 00ab1610d..26bd01627 100644 --- a/backend/src/services/identity-ua/identity-ua-service.ts +++ b/backend/src/services/identity-ua/identity-ua-service.ts @@ -1,4 +1,5 @@ import { ForbiddenError } from "@casl/ability"; +import { requestContext } from "@fastify/request-context"; import { AccessScope, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; @@ -21,6 +22,7 @@ import { } from "@app/lib/errors"; import { checkIPAgainstBlocklist, extractIPDetails, isValidIpOrCidr, TIp } from "@app/lib/ip"; import { logger } from "@app/lib/logger"; +import { AuthAttemptAuthMethod, AuthAttemptAuthResult, authAttemptCounter } from "@app/lib/telemetry/metrics"; import { ActorType, AuthTokenType } from "../auth/auth-type"; import { TIdentityDALFactory } from "../identity/identity-dal"; @@ -77,6 +79,7 @@ export const identityUaServiceFactory = ({ identityDAL }: TIdentityUaServiceFactoryDep) => { const login = async (clientId: string, clientSecret: string, ip: string) => { + const appCfg = getConfig(); const identityUa = await identityUaDAL.findOne({ clientId }); if (!identityUa) { throw new UnauthorizedError({ @@ -84,196 +87,226 @@ export const identityUaServiceFactory = ({ }); } - checkIPAgainstBlocklist({ - ipAddress: ip, - trustedIps: identityUa.clientSecretTrustedIps as TIp[] - }); + const identity = await identityDAL.findById(identityUa.identityId); + const org = await orgDAL.findById(identity.orgId); - const LOCKOUT_KEY = `lockout:identity:${identityUa.identityId}:${IdentityAuthMethod.UNIVERSAL_AUTH}:${clientId}`; - - const lockoutRaw = await keyStore.getItem(LOCKOUT_KEY); - - let lockout: LockoutObject | undefined; - if (lockoutRaw) { - lockout = JSON.parse(lockoutRaw) as LockoutObject; - } - - if (lockout && lockout.lockedOut) { - throw new UnauthorizedError({ - message: "This identity auth method is temporarily locked, please try again later" + try { + checkIPAgainstBlocklist({ + ipAddress: ip, + trustedIps: identityUa.clientSecretTrustedIps as TIp[] }); - } - const clientSecretPrefix = clientSecret.slice(0, 4); - const clientSecretInfo = await identityUaClientSecretDAL.find({ - identityUAId: identityUa.id, - isClientSecretRevoked: false, - clientSecretPrefix - }); + const LOCKOUT_KEY = `lockout:identity:${identityUa.identityId}:${IdentityAuthMethod.UNIVERSAL_AUTH}:${clientId}`; - let validClientSecretInfo: (typeof clientSecretInfo)[0] | null = null; - for await (const info of clientSecretInfo) { - const isMatch = await crypto.hashing().compareHash(clientSecret, info.clientSecretHash); + const lockoutRaw = await keyStore.getItem(LOCKOUT_KEY); - if (isMatch) { - validClientSecretInfo = info; - break; + let lockout: LockoutObject | undefined; + if (lockoutRaw) { + lockout = JSON.parse(lockoutRaw) as LockoutObject; } - } - if (!validClientSecretInfo) { - if (identityUa.lockoutEnabled) { - let lock: Awaited> | undefined; - try { - lock = await keyStore.acquireLock([KeyStorePrefixes.IdentityLockoutLock(LOCKOUT_KEY)], 300, { - retryCount: 3, - retryDelay: 300, - retryJitter: 100 - }); + if (lockout && lockout.lockedOut) { + throw new UnauthorizedError({ + message: "This identity auth method is temporarily locked, please try again later" + }); + } - // Re-fetch the latest lockout data while holding the lock - const lockoutRawNew = await keyStore.getItem(LOCKOUT_KEY); - if (lockoutRawNew) { - lockout = JSON.parse(lockoutRawNew) as LockoutObject; - } else { - lockout = { - lockedOut: false, - failedAttempts: 0 - }; - } + const clientSecretPrefix = clientSecret.slice(0, 4); + const clientSecretInfo = await identityUaClientSecretDAL.find({ + identityUAId: identityUa.id, + isClientSecretRevoked: false, + clientSecretPrefix + }); - if (lockout.lockedOut) { - throw new UnauthorizedError({ - message: "This identity auth method is temporarily locked, please try again later" - }); - } + let validClientSecretInfo: (typeof clientSecretInfo)[0] | null = null; + for await (const info of clientSecretInfo) { + const isMatch = await crypto.hashing().compareHash(clientSecret, info.clientSecretHash); - lockout.failedAttempts += 1; - if (lockout.failedAttempts >= identityUa.lockoutThreshold) { - lockout.lockedOut = true; - } - - await keyStore.setItemWithExpiry( - LOCKOUT_KEY, - lockout.lockedOut ? identityUa.lockoutDurationSeconds : identityUa.lockoutCounterResetSeconds, - JSON.stringify(lockout) - ); - } catch (e) { - if (lock === undefined) { - logger.info( - `identity login failed to acquire lock [identityId=${identityUa.identityId}] [authMethod=${IdentityAuthMethod.UNIVERSAL_AUTH}]` - ); - throw new RateLimitError({ message: "Failed to acquire lock: rate limit exceeded" }); - } - throw e; - } finally { - if (lock) { - await lock.release(); - } + if (isMatch) { + validClientSecretInfo = info; + break; } } - throw new UnauthorizedError({ message: "Invalid credentials" }); - } else if (lockout) { - // If credentials are valid, clear any existing lockout record - await keyStore.deleteItem(LOCKOUT_KEY); - } + if (!validClientSecretInfo) { + if (identityUa.lockoutEnabled) { + let lock: Awaited> | undefined; + try { + lock = await keyStore.acquireLock([KeyStorePrefixes.IdentityLockoutLock(LOCKOUT_KEY)], 300, { + retryCount: 3, + retryDelay: 300, + retryJitter: 100 + }); - const { clientSecretTTL, clientSecretNumUses, clientSecretNumUsesLimit } = validClientSecretInfo; - if (Number(clientSecretTTL) > 0) { - const clientSecretCreated = new Date(validClientSecretInfo.createdAt); - const ttlInMilliseconds = Number(clientSecretTTL) * 1000; - const currentDate = new Date(); - const expirationTime = new Date(clientSecretCreated.getTime() + ttlInMilliseconds); + // Re-fetch the latest lockout data while holding the lock + const lockoutRawNew = await keyStore.getItem(LOCKOUT_KEY); + if (lockoutRawNew) { + lockout = JSON.parse(lockoutRawNew) as LockoutObject; + } else { + lockout = { + lockedOut: false, + failedAttempts: 0 + }; + } - if (currentDate > expirationTime) { + if (lockout.lockedOut) { + throw new UnauthorizedError({ + message: "This identity auth method is temporarily locked, please try again later" + }); + } + + lockout.failedAttempts += 1; + if (lockout.failedAttempts >= identityUa.lockoutThreshold) { + lockout.lockedOut = true; + } + + await keyStore.setItemWithExpiry( + LOCKOUT_KEY, + lockout.lockedOut ? identityUa.lockoutDurationSeconds : identityUa.lockoutCounterResetSeconds, + JSON.stringify(lockout) + ); + } catch (e) { + if (lock === undefined) { + logger.info( + `identity login failed to acquire lock [identityId=${identityUa.identityId}] [authMethod=${IdentityAuthMethod.UNIVERSAL_AUTH}]` + ); + throw new RateLimitError({ message: "Failed to acquire lock: rate limit exceeded" }); + } + throw e; + } finally { + if (lock) { + await lock.release(); + } + } + } + + throw new UnauthorizedError({ message: "Invalid credentials" }); + } else if (lockout) { + // If credentials are valid, clear any existing lockout record + await keyStore.deleteItem(LOCKOUT_KEY); + } + + const { clientSecretTTL, clientSecretNumUses, clientSecretNumUsesLimit } = validClientSecretInfo; + if (Number(clientSecretTTL) > 0) { + const clientSecretCreated = new Date(validClientSecretInfo.createdAt); + const ttlInMilliseconds = Number(clientSecretTTL) * 1000; + const currentDate = new Date(); + const expirationTime = new Date(clientSecretCreated.getTime() + ttlInMilliseconds); + + if (currentDate > expirationTime) { + await identityUaClientSecretDAL.updateById(validClientSecretInfo.id, { + isClientSecretRevoked: true + }); + + throw new UnauthorizedError({ + message: "Access denied due to expired client secret" + }); + } + } + + if (clientSecretNumUsesLimit > 0 && clientSecretNumUses >= clientSecretNumUsesLimit) { + // number of times client secret can be used for + // a login operation reached await identityUaClientSecretDAL.updateById(validClientSecretInfo.id, { isClientSecretRevoked: true }); - throw new UnauthorizedError({ - message: "Access denied due to expired client secret" + message: "Access denied due to client secret usage limit reached" }); } - } - if (clientSecretNumUsesLimit > 0 && clientSecretNumUses >= clientSecretNumUsesLimit) { - // number of times client secret can be used for - // a login operation reached - await identityUaClientSecretDAL.updateById(validClientSecretInfo.id, { - isClientSecretRevoked: true + const accessTokenTTLParams = + Number(identityUa.accessTokenPeriod) === 0 + ? { + accessTokenTTL: identityUa.accessTokenTTL, + accessTokenMaxTTL: identityUa.accessTokenMaxTTL + } + : { + accessTokenTTL: identityUa.accessTokenPeriod, + // We set a very large Max TTL for periodic tokens to ensure that clients (even outdated ones) can always renew their token + // without them having to update their SDKs, CLIs, etc. This workaround sets it to 30 years to emulate "forever" + accessTokenMaxTTL: 1000000000 + }; + + const identityAccessToken = await identityUaDAL.transaction(async (tx) => { + const uaClientSecretDoc = await identityUaClientSecretDAL.incrementUsage(validClientSecretInfo!.id, tx); + await membershipIdentityDAL.update( + { scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id }, + { + lastLoginAuthMethod: IdentityAuthMethod.UNIVERSAL_AUTH, + lastLoginTime: new Date() + }, + tx + ); + const newToken = await identityAccessTokenDAL.create( + { + identityId: identityUa.identityId, + isAccessTokenRevoked: false, + identityUAClientSecretId: uaClientSecretDoc.id, + accessTokenNumUses: 0, + accessTokenNumUsesLimit: identityUa.accessTokenNumUsesLimit, + accessTokenPeriod: identityUa.accessTokenPeriod, + authMethod: IdentityAuthMethod.UNIVERSAL_AUTH, + ...accessTokenTTLParams + }, + tx + ); + + return newToken; }); - throw new UnauthorizedError({ - message: "Access denied due to client secret usage limit reached" - }); - } - const accessTokenTTLParams = - Number(identityUa.accessTokenPeriod) === 0 - ? { - accessTokenTTL: identityUa.accessTokenTTL, - accessTokenMaxTTL: identityUa.accessTokenMaxTTL - } - : { - accessTokenTTL: identityUa.accessTokenPeriod, - // We set a very large Max TTL for periodic tokens to ensure that clients (even outdated ones) can always renew their token - // without them having to update their SDKs, CLIs, etc. This workaround sets it to 30 years to emulate "forever" - accessTokenMaxTTL: 1000000000 - }; - - const identity = await identityDAL.findById(identityUa.identityId); - const identityAccessToken = await identityUaDAL.transaction(async (tx) => { - const uaClientSecretDoc = await identityUaClientSecretDAL.incrementUsage(validClientSecretInfo!.id, tx); - await membershipIdentityDAL.update( - { scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id }, - { - lastLoginAuthMethod: IdentityAuthMethod.UNIVERSAL_AUTH, - lastLoginTime: new Date() - }, - tx - ); - const newToken = await identityAccessTokenDAL.create( + const accessToken = crypto.jwt().sign( { identityId: identityUa.identityId, - isAccessTokenRevoked: false, - identityUAClientSecretId: uaClientSecretDoc.id, - accessTokenNumUses: 0, - accessTokenNumUsesLimit: identityUa.accessTokenNumUsesLimit, - accessTokenPeriod: identityUa.accessTokenPeriod, - authMethod: IdentityAuthMethod.UNIVERSAL_AUTH, - ...accessTokenTTLParams - }, - tx + clientSecretId: validClientSecretInfo.id, + identityAccessTokenId: identityAccessToken.id, + authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN + } as TIdentityAccessTokenJwtPayload, + appCfg.AUTH_SECRET, + // akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error + Number(identityAccessToken.accessTokenTTL) === 0 + ? undefined + : { + expiresIn: Number(identityAccessToken.accessTokenTTL) + } ); - return newToken; - }); + if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) { + authAttemptCounter.add(1, { + "infisical.identity.id": identityUa.identityId, + "infisical.identity.name": identity.name, + "infisical.organization.id": org.id, + "infisical.organization.name": org.name, + "infisical.identity.auth_method": AuthAttemptAuthMethod.UNIVERSAL_AUTH, + "infisical.identity.auth_result": AuthAttemptAuthResult.SUCCESS, + "client.address": requestContext.get("ip"), + "user_agent.original": requestContext.get("userAgent") + }); + } - const appCfg = getConfig(); - const accessToken = crypto.jwt().sign( - { - identityId: identityUa.identityId, - clientSecretId: validClientSecretInfo.id, - identityAccessTokenId: identityAccessToken.id, - authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN - } as TIdentityAccessTokenJwtPayload, - appCfg.AUTH_SECRET, - // akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error - Number(identityAccessToken.accessTokenTTL) === 0 - ? undefined - : { - expiresIn: Number(identityAccessToken.accessTokenTTL) - } - ); - - return { - accessToken, - identityUa, - validClientSecretInfo, - identityAccessToken, - identity, - ...accessTokenTTLParams - }; + return { + accessToken, + identityUa, + validClientSecretInfo, + identityAccessToken, + identity, + ...accessTokenTTLParams + }; + } catch (error) { + if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) { + authAttemptCounter.add(1, { + "infisical.identity.id": identityUa.identityId, + "infisical.identity.name": identity.name, + "infisical.organization.id": org.id, + "infisical.organization.name": org.name, + "infisical.identity.auth_method": AuthAttemptAuthMethod.UNIVERSAL_AUTH, + "infisical.identity.auth_result": AuthAttemptAuthResult.FAILURE, + "client.address": requestContext.get("ip"), + "user_agent.original": requestContext.get("userAgent") + }); + } + throw error; + } }; const attachUniversalAuth = async ({