From e917b744f400251299f2b3ba54f9efb32f6705c0 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard <62331820+DanielHougaard@users.noreply.github.com> Date: Sat, 24 Feb 2024 05:03:08 +0100 Subject: [PATCH] feat: standardize org ID's on auth requests --- .../server/plugins/auth/inject-identity.ts | 36 +++++++++++++++- .../server/plugins/auth/inject-permission.ts | 4 +- backend/src/server/routes/index.ts | 8 +++- .../services/auth-token/auth-token-service.ts | 23 +++++++++-- .../identity-access-token-service.ts | 15 ++++++- .../service-token/service-token-service.ts | 9 +++- frontend/src/config/request.ts | 41 +++++++++++-------- 7 files changed, 106 insertions(+), 30 deletions(-) diff --git a/backend/src/server/plugins/auth/inject-identity.ts b/backend/src/server/plugins/auth/inject-identity.ts index 3a0a0ab39..d71bca1bd 100644 --- a/backend/src/server/plugins/auth/inject-identity.ts +++ b/backend/src/server/plugins/auth/inject-identity.ts @@ -11,12 +11,12 @@ import { TIdentityAccessTokenJwtPayload } from "@app/services/identity-access-to export type TAuthMode = | { - orgId?: string; authMode: AuthMode.JWT; actor: ActorType.USER; userId: string; tokenVersionId: string; // the session id of token used user: TUsers; + orgId?: string; } | { authMode: AuthMode.API_KEY; @@ -30,12 +30,14 @@ export type TAuthMode = serviceToken: TServiceTokens & { createdByEmail: string }; actor: ActorType.SERVICE; serviceTokenId: string; + orgId: string; } | { authMode: AuthMode.IDENTITY_ACCESS_TOKEN; actor: ActorType.IDENTITY; identityId: string; identityName: string; + orgId: string; } | { authMode: AuthMode.SCIM_TOKEN; @@ -89,6 +91,26 @@ const extractAuth = async (req: FastifyRequest, jwtSecret: string) => { } }; +/* +!!! IMPORTANT NOTE ABOUT `orgId` FIELD on `req.auth` !!! + +The `orgId` is an optional field, this is intentional. +There are cases where the `orgId` won't be present on the request auth object. + + +2 Examples: + +1. When a user first creates their account, no organization is present most of the time, because they haven't created one yet. +2. When a user is using an API key. We can't link API keys to organizations, because they are not tied to any organization, but instead they're tied to the user itself. + + +Reasons for orgId to be undefined when JWT is used, is to indicate that a certain token was obtained from successfully logging into an org with org-level auth enforced. +Certain organizations don’t require that enforcement and so the tokens don’t have organizationId on them. +They shouldn’t be used to access organizations that have specific org-level auth enforced +And so to differentiate between tokens that were obtained from regular login vs those at the org-auth level we include that field into those tokens. + +*/ + export const injectIdentity = fp(async (server: FastifyZodProvider) => { server.decorateRequest("auth", null); server.addHook("onRequest", async (req) => { @@ -97,36 +119,46 @@ export const injectIdentity = fp(async (server: FastifyZodProvider) => { if (!authMode) return; switch (authMode) { + // May or may not have an orgId. If it doesn't have an org ID, it's likely because the token is from an org that doesn't enforce org-level auth. case AuthMode.JWT: { - const { user, tokenVersionId, orgId } = await server.services.authToken.fnValidateJwtIdentity(token); + const { user, tokenVersionId, orgId } = await server.services.authToken.fnValidateJwtIdentity( + token, + req.headers?.["x-infisical-organization-id"] + ); req.auth = { authMode: AuthMode.JWT, user, userId: user.id, tokenVersionId, actor, orgId }; break; } + // Will always contain an orgId. case AuthMode.IDENTITY_ACCESS_TOKEN: { const identity = await server.services.identityAccessToken.fnValidateIdentityAccessToken(token, req.realIp); req.auth = { authMode: AuthMode.IDENTITY_ACCESS_TOKEN, actor, + orgId: identity.orgId, identityId: identity.identityId, identityName: identity.name }; break; } + // Will always contain an orgId. case AuthMode.SERVICE_TOKEN: { const serviceToken = await server.services.serviceToken.fnValidateServiceToken(token); req.auth = { authMode: AuthMode.SERVICE_TOKEN as const, serviceToken, + orgId: serviceToken.orgId, serviceTokenId: serviceToken.id, actor }; break; } + // Will never contain an orgId. API keys are not tied to an organization. case AuthMode.API_KEY: { const user = await server.services.apiKey.fnValidateApiKey(token as string); req.auth = { authMode: AuthMode.API_KEY as const, userId: user.id, actor, user }; break; } + // OK case AuthMode.SCIM_TOKEN: { const { orgId, scimTokenId } = await server.services.scim.fnValidateScimToken(token); req.auth = { authMode: AuthMode.SCIM_TOKEN, actor, scimTokenId, orgId }; diff --git a/backend/src/server/plugins/auth/inject-permission.ts b/backend/src/server/plugins/auth/inject-permission.ts index 2d61647e8..bd4ac66db 100644 --- a/backend/src/server/plugins/auth/inject-permission.ts +++ b/backend/src/server/plugins/auth/inject-permission.ts @@ -11,9 +11,9 @@ export const injectPermission = fp(async (server) => { if (req.auth.actor === ActorType.USER) { req.permission = { type: ActorType.USER, id: req.auth.userId, orgId: req.auth?.orgId }; } else if (req.auth.actor === ActorType.IDENTITY) { - req.permission = { type: ActorType.IDENTITY, id: req.auth.identityId }; + req.permission = { type: ActorType.IDENTITY, id: req.auth.identityId, orgId: req.auth.orgId }; } else if (req.auth.actor === ActorType.SERVICE) { - req.permission = { type: ActorType.SERVICE, id: req.auth.serviceTokenId }; + req.permission = { type: ActorType.SERVICE, id: req.auth.serviceTokenId, orgId: req.auth.orgId }; } else if (req.auth.actor === ActorType.SCIM_CLIENT) { req.permission = { type: ActorType.SCIM_CLIENT, id: req.auth.scimTokenId, orgId: req.auth.orgId }; } diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 0a49806bd..7f254d3be 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -264,7 +264,7 @@ export const registerRoutes = async ( queueService }); - const tokenService = tokenServiceFactory({ tokenDAL: authTokenDAL, userDAL }); + const tokenService = tokenServiceFactory({ tokenDAL: authTokenDAL, userDAL, orgDAL }); const userService = userServiceFactory({ userDAL }); const loginService = authLoginServiceFactory({ userDAL, smtpService, tokenService }); const passwordService = authPaswordServiceFactory({ @@ -516,6 +516,7 @@ export const registerRoutes = async ( const serviceTokenService = serviceTokenServiceFactory({ projectEnvDAL, serviceTokenDAL, + orgDAL, userDAL, permissionService }); @@ -525,7 +526,10 @@ export const registerRoutes = async ( identityDAL, identityOrgMembershipDAL }); - const identityAccessTokenService = identityAccessTokenServiceFactory({ identityAccessTokenDAL }); + const identityAccessTokenService = identityAccessTokenServiceFactory({ + identityAccessTokenDAL, + identityOrgMembershipDAL + }); const identityProjectService = identityProjectServiceFactory({ permissionService, projectDAL, diff --git a/backend/src/services/auth-token/auth-token-service.ts b/backend/src/services/auth-token/auth-token-service.ts index 59f336e5a..4c44a101b 100644 --- a/backend/src/services/auth-token/auth-token-service.ts +++ b/backend/src/services/auth-token/auth-token-service.ts @@ -7,6 +7,7 @@ import { getConfig } from "@app/lib/config/env"; import { UnauthorizedError } from "@app/lib/errors"; import { AuthModeJwtTokenPayload } from "../auth/auth-type"; +import { TOrgDALFactory } from "../org/org-dal"; import { TUserDALFactory } from "../user/user-dal"; import { TTokenDALFactory } from "./auth-token-dal"; import { TCreateTokenForUserDTO, TIssueAuthTokenDTO, TokenType, TValidateTokenForUserDTO } from "./auth-token-types"; @@ -14,6 +15,7 @@ import { TCreateTokenForUserDTO, TIssueAuthTokenDTO, TokenType, TValidateTokenFo type TAuthTokenServiceFactoryDep = { tokenDAL: TTokenDALFactory; userDAL: Pick; + orgDAL: Pick; }; export type TAuthTokenServiceFactory = ReturnType; @@ -54,7 +56,7 @@ export const getTokenConfig = (tokenType: TokenType) => { } }; -export const tokenServiceFactory = ({ tokenDAL, userDAL }: TAuthTokenServiceFactoryDep) => { +export const tokenServiceFactory = ({ tokenDAL, userDAL, orgDAL }: TAuthTokenServiceFactoryDep) => { const createTokenForUser = async ({ type, userId, orgId }: TCreateTokenForUserDTO) => { const { token, ...tkCfg } = getTokenConfig(type); const appCfg = getConfig(); @@ -130,7 +132,7 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL }: TAuthTokenServiceFact const revokeAllMySessions = async (userId: string) => tokenDAL.deleteTokenSession({ userId }); // to parse jwt identity in inject identity plugin - const fnValidateJwtIdentity = async (token: AuthModeJwtTokenPayload) => { + const fnValidateJwtIdentity = async (token: AuthModeJwtTokenPayload, organizationIdHeader?: string | string[]) => { const session = await tokenDAL.findOneTokenSession({ id: token.tokenVersionId, userId: token.userId @@ -141,7 +143,22 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL }: TAuthTokenServiceFact const user = await userDAL.findById(session.userId); if (!user || !user.isAccepted) throw new UnauthorizedError({ name: "Token user not found" }); - return { user, tokenVersionId: token.tokenVersionId, orgId: token.organizationId }; + let orgId = token.organizationId; + if (!token.organizationId && organizationIdHeader) { + // If the token doesn't have an organization ID, but an organization ID is provided in the header, we need to check if the user is a member of the organization before concluding the organization ID is valid. + const userMembership = ( + await orgDAL.findMembership({ + userId: user.id, + orgId: organizationIdHeader as string + }) + )[0]; + + if (!userMembership) throw new UnauthorizedError({ name: "User not a member of the organization" }); + + orgId = userMembership.orgId; + } + + return { user, tokenVersionId: token.tokenVersionId, orgId }; }; return { diff --git a/backend/src/services/identity-access-token/identity-access-token-service.ts b/backend/src/services/identity-access-token/identity-access-token-service.ts index 32774ccbb..4b53c8174 100644 --- a/backend/src/services/identity-access-token/identity-access-token-service.ts +++ b/backend/src/services/identity-access-token/identity-access-token-service.ts @@ -6,17 +6,20 @@ import { BadRequestError, UnauthorizedError } from "@app/lib/errors"; import { checkIPAgainstBlocklist, TIp } from "@app/lib/ip"; import { AuthTokenType } from "../auth/auth-type"; +import { TIdentityOrgDALFactory } from "../identity/identity-org-dal"; import { TIdentityAccessTokenDALFactory } from "./identity-access-token-dal"; import { TIdentityAccessTokenJwtPayload, TRenewAccessTokenDTO } from "./identity-access-token-types"; type TIdentityAccessTokenServiceFactoryDep = { identityAccessTokenDAL: TIdentityAccessTokenDALFactory; + identityOrgMembershipDAL: TIdentityOrgDALFactory; }; export type TIdentityAccessTokenServiceFactory = ReturnType; export const identityAccessTokenServiceFactory = ({ - identityAccessTokenDAL + identityAccessTokenDAL, + identityOrgMembershipDAL }: TIdentityAccessTokenServiceFactoryDep) => { const validateAccessTokenExp = (identityAccessToken: TIdentityAccessTokens) => { const { @@ -117,8 +120,16 @@ export const identityAccessTokenServiceFactory = ({ }); } + const identityOrgMembership = await identityOrgMembershipDAL.findOne({ + identityId: identityAccessToken.identityId + }); + + if (!identityOrgMembership) { + throw new UnauthorizedError({ message: "Identity does not belong to any organization" }); + } + validateAccessTokenExp(identityAccessToken); - return identityAccessToken; + return { ...identityAccessToken, orgId: identityOrgMembership.orgId }; }; return { renewAccessToken, fnValidateIdentityAccessToken }; diff --git a/backend/src/services/service-token/service-token-service.ts b/backend/src/services/service-token/service-token-service.ts index cce0d3780..748fbc95d 100644 --- a/backend/src/services/service-token/service-token-service.ts +++ b/backend/src/services/service-token/service-token-service.ts @@ -9,6 +9,7 @@ import { getConfig } from "@app/lib/config/env"; import { BadRequestError, UnauthorizedError } from "@app/lib/errors"; import { ActorType } from "../auth/auth-type"; +import { TOrgDALFactory } from "../org/org-dal"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; import { TUserDALFactory } from "../user/user-dal"; import { TServiceTokenDALFactory } from "./service-token-dal"; @@ -23,6 +24,7 @@ type TServiceTokenServiceFactoryDep = { serviceTokenDAL: TServiceTokenDALFactory; userDAL: TUserDALFactory; permissionService: Pick; + orgDAL: Pick; projectEnvDAL: Pick; }; @@ -31,6 +33,7 @@ export type TServiceTokenServiceFactory = ReturnType { @@ -130,6 +133,7 @@ export const serviceTokenServiceFactory = ({ const fnValidateServiceToken = async (token: string) => { const [, TOKEN_IDENTIFIER, TOKEN_SECRET] = <[string, string, string]>token.split(".", 3); const serviceToken = await serviceTokenDAL.findById(TOKEN_IDENTIFIER); + if (!serviceToken) throw new UnauthorizedError(); if (serviceToken.expiresAt && new Date(serviceToken.expiresAt) < new Date()) { @@ -142,7 +146,10 @@ export const serviceTokenServiceFactory = ({ const updatedToken = await serviceTokenDAL.updateById(serviceToken.id, { lastUsed: new Date() }); - return { ...serviceToken, lastUsed: updatedToken.lastUsed }; + + const organization = await orgDAL.findOrgByProjectId(serviceToken.projectId); + + return { ...serviceToken, lastUsed: updatedToken.lastUsed, orgId: organization.id }; }; return { diff --git a/frontend/src/config/request.ts b/frontend/src/config/request.ts index 8f8669010..0d0525799 100644 --- a/frontend/src/config/request.ts +++ b/frontend/src/config/request.ts @@ -1,11 +1,7 @@ import axios from "axios"; import SecurityClient from "@app/components/utilities/SecurityClient"; -import { - getAuthToken, - getMfaTempToken, - getSignupTempToken -} from "@app/reactQuery"; +import { getAuthToken, getMfaTempToken, getSignupTempToken } from "@app/reactQuery"; export const apiRequest = axios.create({ baseURL: "/", @@ -19,19 +15,28 @@ apiRequest.interceptors.request.use((config) => { const mfaTempToken = getMfaTempToken(); const token = getAuthToken(); const providerAuthToken = SecurityClient.getProviderAuthToken(); - - if (signupTempToken && config.headers) { - // eslint-disable-next-line no-param-reassign - config.headers.Authorization = `Bearer ${signupTempToken}`; - } else if (mfaTempToken && config.headers) { - // eslint-disable-next-line no-param-reassign - config.headers.Authorization = `Bearer ${mfaTempToken}`; - } else if (token && config.headers) { - // eslint-disable-next-line no-param-reassign - config.headers.Authorization = `Bearer ${token}`; - } else if(providerAuthToken && config.headers) { - // eslint-disable-next-line no-param-reassign - config.headers.Authorization = `Bearer ${providerAuthToken}`; + const organizationId = localStorage.getItem("orgData.id"); + + if (config.headers) { + if (organizationId) { + // eslint-disable-next-line no-param-reassign + config.headers["x-infisical-organization-id"] = organizationId; + } + + if (signupTempToken) { + // eslint-disable-next-line no-param-reassign + config.headers.Authorization = `Bearer ${signupTempToken}`; + } else if (mfaTempToken) { + // eslint-disable-next-line no-param-reassign + config.headers.Authorization = `Bearer ${mfaTempToken}`; + } else if (token) { + // eslint-disable-next-line no-param-reassign + config.headers.Authorization = `Bearer ${token}`; + } else if (providerAuthToken) { + // eslint-disable-next-line no-param-reassign + config.headers.Authorization = `Bearer ${providerAuthToken}`; + } } + return config; });