feat: standardize org ID's on auth requests

This commit is contained in:
Daniel Hougaard
2024-03-17 18:48:09 +01:00
parent 7438c114dd
commit e917b744f4
7 changed files with 106 additions and 30 deletions
@@ -11,12 +11,12 @@ import { TIdentityAccessTokenJwtPayload } from "@app/services/identity-access-to
export type TAuthMode = export type TAuthMode =
| { | {
orgId?: string;
authMode: AuthMode.JWT; authMode: AuthMode.JWT;
actor: ActorType.USER; actor: ActorType.USER;
userId: string; userId: string;
tokenVersionId: string; // the session id of token used tokenVersionId: string; // the session id of token used
user: TUsers; user: TUsers;
orgId?: string;
} }
| { | {
authMode: AuthMode.API_KEY; authMode: AuthMode.API_KEY;
@@ -30,12 +30,14 @@ export type TAuthMode =
serviceToken: TServiceTokens & { createdByEmail: string }; serviceToken: TServiceTokens & { createdByEmail: string };
actor: ActorType.SERVICE; actor: ActorType.SERVICE;
serviceTokenId: string; serviceTokenId: string;
orgId: string;
} }
| { | {
authMode: AuthMode.IDENTITY_ACCESS_TOKEN; authMode: AuthMode.IDENTITY_ACCESS_TOKEN;
actor: ActorType.IDENTITY; actor: ActorType.IDENTITY;
identityId: string; identityId: string;
identityName: string; identityName: string;
orgId: string;
} }
| { | {
authMode: AuthMode.SCIM_TOKEN; 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) => { export const injectIdentity = fp(async (server: FastifyZodProvider) => {
server.decorateRequest("auth", null); server.decorateRequest("auth", null);
server.addHook("onRequest", async (req) => { server.addHook("onRequest", async (req) => {
@@ -97,36 +119,46 @@ export const injectIdentity = fp(async (server: FastifyZodProvider) => {
if (!authMode) return; if (!authMode) return;
switch (authMode) { 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: { 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 }; req.auth = { authMode: AuthMode.JWT, user, userId: user.id, tokenVersionId, actor, orgId };
break; break;
} }
// Will always contain an orgId.
case AuthMode.IDENTITY_ACCESS_TOKEN: { case AuthMode.IDENTITY_ACCESS_TOKEN: {
const identity = await server.services.identityAccessToken.fnValidateIdentityAccessToken(token, req.realIp); const identity = await server.services.identityAccessToken.fnValidateIdentityAccessToken(token, req.realIp);
req.auth = { req.auth = {
authMode: AuthMode.IDENTITY_ACCESS_TOKEN, authMode: AuthMode.IDENTITY_ACCESS_TOKEN,
actor, actor,
orgId: identity.orgId,
identityId: identity.identityId, identityId: identity.identityId,
identityName: identity.name identityName: identity.name
}; };
break; break;
} }
// Will always contain an orgId.
case AuthMode.SERVICE_TOKEN: { case AuthMode.SERVICE_TOKEN: {
const serviceToken = await server.services.serviceToken.fnValidateServiceToken(token); const serviceToken = await server.services.serviceToken.fnValidateServiceToken(token);
req.auth = { req.auth = {
authMode: AuthMode.SERVICE_TOKEN as const, authMode: AuthMode.SERVICE_TOKEN as const,
serviceToken, serviceToken,
orgId: serviceToken.orgId,
serviceTokenId: serviceToken.id, serviceTokenId: serviceToken.id,
actor actor
}; };
break; break;
} }
// Will never contain an orgId. API keys are not tied to an organization.
case AuthMode.API_KEY: { case AuthMode.API_KEY: {
const user = await server.services.apiKey.fnValidateApiKey(token as string); const user = await server.services.apiKey.fnValidateApiKey(token as string);
req.auth = { authMode: AuthMode.API_KEY as const, userId: user.id, actor, user }; req.auth = { authMode: AuthMode.API_KEY as const, userId: user.id, actor, user };
break; break;
} }
// OK
case AuthMode.SCIM_TOKEN: { case AuthMode.SCIM_TOKEN: {
const { orgId, scimTokenId } = await server.services.scim.fnValidateScimToken(token); const { orgId, scimTokenId } = await server.services.scim.fnValidateScimToken(token);
req.auth = { authMode: AuthMode.SCIM_TOKEN, actor, scimTokenId, orgId }; req.auth = { authMode: AuthMode.SCIM_TOKEN, actor, scimTokenId, orgId };
@@ -11,9 +11,9 @@ export const injectPermission = fp(async (server) => {
if (req.auth.actor === ActorType.USER) { if (req.auth.actor === ActorType.USER) {
req.permission = { type: ActorType.USER, id: req.auth.userId, orgId: req.auth?.orgId }; req.permission = { type: ActorType.USER, id: req.auth.userId, orgId: req.auth?.orgId };
} else if (req.auth.actor === ActorType.IDENTITY) { } 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) { } 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) { } else if (req.auth.actor === ActorType.SCIM_CLIENT) {
req.permission = { type: ActorType.SCIM_CLIENT, id: req.auth.scimTokenId, orgId: req.auth.orgId }; req.permission = { type: ActorType.SCIM_CLIENT, id: req.auth.scimTokenId, orgId: req.auth.orgId };
} }
+6 -2
View File
@@ -264,7 +264,7 @@ export const registerRoutes = async (
queueService queueService
}); });
const tokenService = tokenServiceFactory({ tokenDAL: authTokenDAL, userDAL }); const tokenService = tokenServiceFactory({ tokenDAL: authTokenDAL, userDAL, orgDAL });
const userService = userServiceFactory({ userDAL }); const userService = userServiceFactory({ userDAL });
const loginService = authLoginServiceFactory({ userDAL, smtpService, tokenService }); const loginService = authLoginServiceFactory({ userDAL, smtpService, tokenService });
const passwordService = authPaswordServiceFactory({ const passwordService = authPaswordServiceFactory({
@@ -516,6 +516,7 @@ export const registerRoutes = async (
const serviceTokenService = serviceTokenServiceFactory({ const serviceTokenService = serviceTokenServiceFactory({
projectEnvDAL, projectEnvDAL,
serviceTokenDAL, serviceTokenDAL,
orgDAL,
userDAL, userDAL,
permissionService permissionService
}); });
@@ -525,7 +526,10 @@ export const registerRoutes = async (
identityDAL, identityDAL,
identityOrgMembershipDAL identityOrgMembershipDAL
}); });
const identityAccessTokenService = identityAccessTokenServiceFactory({ identityAccessTokenDAL }); const identityAccessTokenService = identityAccessTokenServiceFactory({
identityAccessTokenDAL,
identityOrgMembershipDAL
});
const identityProjectService = identityProjectServiceFactory({ const identityProjectService = identityProjectServiceFactory({
permissionService, permissionService,
projectDAL, projectDAL,
@@ -7,6 +7,7 @@ import { getConfig } from "@app/lib/config/env";
import { UnauthorizedError } from "@app/lib/errors"; import { UnauthorizedError } from "@app/lib/errors";
import { AuthModeJwtTokenPayload } from "../auth/auth-type"; import { AuthModeJwtTokenPayload } from "../auth/auth-type";
import { TOrgDALFactory } from "../org/org-dal";
import { TUserDALFactory } from "../user/user-dal"; import { TUserDALFactory } from "../user/user-dal";
import { TTokenDALFactory } from "./auth-token-dal"; import { TTokenDALFactory } from "./auth-token-dal";
import { TCreateTokenForUserDTO, TIssueAuthTokenDTO, TokenType, TValidateTokenForUserDTO } from "./auth-token-types"; import { TCreateTokenForUserDTO, TIssueAuthTokenDTO, TokenType, TValidateTokenForUserDTO } from "./auth-token-types";
@@ -14,6 +15,7 @@ import { TCreateTokenForUserDTO, TIssueAuthTokenDTO, TokenType, TValidateTokenFo
type TAuthTokenServiceFactoryDep = { type TAuthTokenServiceFactoryDep = {
tokenDAL: TTokenDALFactory; tokenDAL: TTokenDALFactory;
userDAL: Pick<TUserDALFactory, "findById">; userDAL: Pick<TUserDALFactory, "findById">;
orgDAL: Pick<TOrgDALFactory, "findMembership">;
}; };
export type TAuthTokenServiceFactory = ReturnType<typeof tokenServiceFactory>; export type TAuthTokenServiceFactory = ReturnType<typeof tokenServiceFactory>;
@@ -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 createTokenForUser = async ({ type, userId, orgId }: TCreateTokenForUserDTO) => {
const { token, ...tkCfg } = getTokenConfig(type); const { token, ...tkCfg } = getTokenConfig(type);
const appCfg = getConfig(); const appCfg = getConfig();
@@ -130,7 +132,7 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL }: TAuthTokenServiceFact
const revokeAllMySessions = async (userId: string) => tokenDAL.deleteTokenSession({ userId }); const revokeAllMySessions = async (userId: string) => tokenDAL.deleteTokenSession({ userId });
// to parse jwt identity in inject identity plugin // 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({ const session = await tokenDAL.findOneTokenSession({
id: token.tokenVersionId, id: token.tokenVersionId,
userId: token.userId userId: token.userId
@@ -141,7 +143,22 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL }: TAuthTokenServiceFact
const user = await userDAL.findById(session.userId); const user = await userDAL.findById(session.userId);
if (!user || !user.isAccepted) throw new UnauthorizedError({ name: "Token user not found" }); 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 { return {
@@ -6,17 +6,20 @@ import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
import { checkIPAgainstBlocklist, TIp } from "@app/lib/ip"; import { checkIPAgainstBlocklist, TIp } from "@app/lib/ip";
import { AuthTokenType } from "../auth/auth-type"; import { AuthTokenType } from "../auth/auth-type";
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
import { TIdentityAccessTokenDALFactory } from "./identity-access-token-dal"; import { TIdentityAccessTokenDALFactory } from "./identity-access-token-dal";
import { TIdentityAccessTokenJwtPayload, TRenewAccessTokenDTO } from "./identity-access-token-types"; import { TIdentityAccessTokenJwtPayload, TRenewAccessTokenDTO } from "./identity-access-token-types";
type TIdentityAccessTokenServiceFactoryDep = { type TIdentityAccessTokenServiceFactoryDep = {
identityAccessTokenDAL: TIdentityAccessTokenDALFactory; identityAccessTokenDAL: TIdentityAccessTokenDALFactory;
identityOrgMembershipDAL: TIdentityOrgDALFactory;
}; };
export type TIdentityAccessTokenServiceFactory = ReturnType<typeof identityAccessTokenServiceFactory>; export type TIdentityAccessTokenServiceFactory = ReturnType<typeof identityAccessTokenServiceFactory>;
export const identityAccessTokenServiceFactory = ({ export const identityAccessTokenServiceFactory = ({
identityAccessTokenDAL identityAccessTokenDAL,
identityOrgMembershipDAL
}: TIdentityAccessTokenServiceFactoryDep) => { }: TIdentityAccessTokenServiceFactoryDep) => {
const validateAccessTokenExp = (identityAccessToken: TIdentityAccessTokens) => { const validateAccessTokenExp = (identityAccessToken: TIdentityAccessTokens) => {
const { 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); validateAccessTokenExp(identityAccessToken);
return identityAccessToken; return { ...identityAccessToken, orgId: identityOrgMembership.orgId };
}; };
return { renewAccessToken, fnValidateIdentityAccessToken }; return { renewAccessToken, fnValidateIdentityAccessToken };
@@ -9,6 +9,7 @@ import { getConfig } from "@app/lib/config/env";
import { BadRequestError, UnauthorizedError } from "@app/lib/errors"; import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
import { ActorType } from "../auth/auth-type"; import { ActorType } from "../auth/auth-type";
import { TOrgDALFactory } from "../org/org-dal";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { TUserDALFactory } from "../user/user-dal"; import { TUserDALFactory } from "../user/user-dal";
import { TServiceTokenDALFactory } from "./service-token-dal"; import { TServiceTokenDALFactory } from "./service-token-dal";
@@ -23,6 +24,7 @@ type TServiceTokenServiceFactoryDep = {
serviceTokenDAL: TServiceTokenDALFactory; serviceTokenDAL: TServiceTokenDALFactory;
userDAL: TUserDALFactory; userDAL: TUserDALFactory;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
orgDAL: Pick<TOrgDALFactory, "findOrgByProjectId">;
projectEnvDAL: Pick<TProjectEnvDALFactory, "findBySlugs">; projectEnvDAL: Pick<TProjectEnvDALFactory, "findBySlugs">;
}; };
@@ -31,6 +33,7 @@ export type TServiceTokenServiceFactory = ReturnType<typeof serviceTokenServiceF
export const serviceTokenServiceFactory = ({ export const serviceTokenServiceFactory = ({
serviceTokenDAL, serviceTokenDAL,
userDAL, userDAL,
orgDAL,
permissionService, permissionService,
projectEnvDAL projectEnvDAL
}: TServiceTokenServiceFactoryDep) => { }: TServiceTokenServiceFactoryDep) => {
@@ -130,6 +133,7 @@ export const serviceTokenServiceFactory = ({
const fnValidateServiceToken = async (token: string) => { const fnValidateServiceToken = async (token: string) => {
const [, TOKEN_IDENTIFIER, TOKEN_SECRET] = <[string, string, string]>token.split(".", 3); const [, TOKEN_IDENTIFIER, TOKEN_SECRET] = <[string, string, string]>token.split(".", 3);
const serviceToken = await serviceTokenDAL.findById(TOKEN_IDENTIFIER); const serviceToken = await serviceTokenDAL.findById(TOKEN_IDENTIFIER);
if (!serviceToken) throw new UnauthorizedError(); if (!serviceToken) throw new UnauthorizedError();
if (serviceToken.expiresAt && new Date(serviceToken.expiresAt) < new Date()) { if (serviceToken.expiresAt && new Date(serviceToken.expiresAt) < new Date()) {
@@ -142,7 +146,10 @@ export const serviceTokenServiceFactory = ({
const updatedToken = await serviceTokenDAL.updateById(serviceToken.id, { const updatedToken = await serviceTokenDAL.updateById(serviceToken.id, {
lastUsed: new Date() lastUsed: new Date()
}); });
return { ...serviceToken, lastUsed: updatedToken.lastUsed };
const organization = await orgDAL.findOrgByProjectId(serviceToken.projectId);
return { ...serviceToken, lastUsed: updatedToken.lastUsed, orgId: organization.id };
}; };
return { return {
+22 -17
View File
@@ -1,11 +1,7 @@
import axios from "axios"; import axios from "axios";
import SecurityClient from "@app/components/utilities/SecurityClient"; import SecurityClient from "@app/components/utilities/SecurityClient";
import { import { getAuthToken, getMfaTempToken, getSignupTempToken } from "@app/reactQuery";
getAuthToken,
getMfaTempToken,
getSignupTempToken
} from "@app/reactQuery";
export const apiRequest = axios.create({ export const apiRequest = axios.create({
baseURL: "/", baseURL: "/",
@@ -19,19 +15,28 @@ apiRequest.interceptors.request.use((config) => {
const mfaTempToken = getMfaTempToken(); const mfaTempToken = getMfaTempToken();
const token = getAuthToken(); const token = getAuthToken();
const providerAuthToken = SecurityClient.getProviderAuthToken(); const providerAuthToken = SecurityClient.getProviderAuthToken();
const organizationId = localStorage.getItem("orgData.id");
if (signupTempToken && config.headers) { if (config.headers) {
// eslint-disable-next-line no-param-reassign if (organizationId) {
config.headers.Authorization = `Bearer ${signupTempToken}`; // eslint-disable-next-line no-param-reassign
} else if (mfaTempToken && config.headers) { config.headers["x-infisical-organization-id"] = organizationId;
// eslint-disable-next-line no-param-reassign }
config.headers.Authorization = `Bearer ${mfaTempToken}`;
} else if (token && config.headers) { if (signupTempToken) {
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
config.headers.Authorization = `Bearer ${token}`; config.headers.Authorization = `Bearer ${signupTempToken}`;
} else if(providerAuthToken && config.headers) { } else if (mfaTempToken) {
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
config.headers.Authorization = `Bearer ${providerAuthToken}`; 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; return config;
}); });