mirror of
https://github.com/Infisical/infisical.git
synced 2025-03-28 15:29:21 +00:00
feat: standardize org ID's on auth requests
This commit is contained in:
@ -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 };
|
||||
|
@ -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 };
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
@ -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 };
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
});
|
||||
|
Reference in New Issue
Block a user