feat: standardize org ID's on auth requests

This commit is contained in:
Daniel Hougaard
2024-02-24 05:03:08 +01:00
parent 7438c114dd
commit e917b744f4
7 changed files with 106 additions and 30 deletions

View File

@ -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 dont require that enforcement and so the tokens dont have organizationId on them.
They shouldnt 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 };

View File

@ -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 };
}

View File

@ -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,

View File

@ -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<TUserDALFactory, "findById">;
orgDAL: Pick<TOrgDALFactory, "findMembership">;
};
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 { 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 {

View File

@ -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<typeof identityAccessTokenServiceFactory>;
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 };

View File

@ -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<TPermissionServiceFactory, "getProjectPermission">;
orgDAL: Pick<TOrgDALFactory, "findOrgByProjectId">;
projectEnvDAL: Pick<TProjectEnvDALFactory, "findBySlugs">;
};
@ -31,6 +33,7 @@ export type TServiceTokenServiceFactory = ReturnType<typeof serviceTokenServiceF
export const serviceTokenServiceFactory = ({
serviceTokenDAL,
userDAL,
orgDAL,
permissionService,
projectEnvDAL
}: TServiceTokenServiceFactoryDep) => {
@ -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 {

View File

@ -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;
});