mirror of
https://github.com/Infisical/infisical.git
synced 2025-03-28 15:29:21 +00:00
Feat: Org Scoped JWT Tokens
This commit is contained in:
@ -1,7 +1,10 @@
|
||||
import jwt from "jsonwebtoken";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { UnauthorizedError } from "@app/lib/errors";
|
||||
import { authRateLimit } from "@app/server/config/rateLimiter";
|
||||
import { AuthModeJwtTokenPayload } from "@app/services/auth/auth-type";
|
||||
|
||||
export const registerLoginRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
@ -34,6 +37,67 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/select-organization",
|
||||
config: {
|
||||
rateLimit: authRateLimit
|
||||
},
|
||||
schema: {
|
||||
body: z.object({
|
||||
organizationId: z.string().trim()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
token: z.string()
|
||||
})
|
||||
}
|
||||
},
|
||||
handler: async (req, res) => {
|
||||
const cfg = getConfig();
|
||||
|
||||
if (!req.headers.authorization) throw new UnauthorizedError({ name: "Authorization header is required" });
|
||||
if (!req.headers["user-agent"]) throw new UnauthorizedError({ name: "user agent header is required" });
|
||||
|
||||
const userAgent = req.headers["user-agent"];
|
||||
const authToken = req.headers.authorization.slice(7); // slice of after Bearer
|
||||
|
||||
// The decoded JWT token, which contains the auth method.
|
||||
const decodedToken = jwt.verify(authToken, cfg.AUTH_SECRET) as AuthModeJwtTokenPayload;
|
||||
|
||||
if (decodedToken.organizationId) {
|
||||
throw new UnauthorizedError({ message: "You have already selected an organization" });
|
||||
}
|
||||
|
||||
const user = await server.services.user.getMe(decodedToken.userId);
|
||||
|
||||
// Check if the user actually has access to the specified organization.
|
||||
const userOrgs = await server.services.org.findAllOrganizationOfUser(user.id);
|
||||
|
||||
if (!userOrgs.some((org) => org.id === req.body.organizationId)) {
|
||||
throw new UnauthorizedError({ message: "User does not have access to the organization" });
|
||||
}
|
||||
|
||||
await server.services.authToken.clearTokenSessionById(decodedToken.userId, decodedToken.tokenVersionId);
|
||||
const tokens = await server.services.login.generateUserTokens({
|
||||
authMethod: decodedToken.authMethod,
|
||||
user,
|
||||
userAgent,
|
||||
ip: req.realIp,
|
||||
organizationId: req.body.organizationId
|
||||
});
|
||||
|
||||
void res.setCookie("jid", tokens.refresh, {
|
||||
httpOnly: true,
|
||||
path: "/",
|
||||
sameSite: "strict",
|
||||
secure: cfg.HTTPS_ENABLED
|
||||
});
|
||||
|
||||
return { token: tokens.access };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/login2",
|
||||
|
@ -6,7 +6,8 @@ import { TAuthTokens, TAuthTokenSessions } from "@app/db/schemas";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { UnauthorizedError } from "@app/lib/errors";
|
||||
|
||||
import { AuthModeJwtTokenPayload } from "../auth/auth-type";
|
||||
import { AuthMethod, 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: TOrgDALFactory;
|
||||
};
|
||||
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();
|
||||
@ -135,12 +137,25 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL }: TAuthTokenServiceFact
|
||||
id: token.tokenVersionId,
|
||||
userId: token.userId
|
||||
});
|
||||
|
||||
if (!session) throw new UnauthorizedError({ name: "Session not found" });
|
||||
if (token.accessVersion !== session.accessVersion) throw new UnauthorizedError({ name: "Stale session" });
|
||||
|
||||
const user = await userDAL.findById(session.userId);
|
||||
if (!user || !user.isAccepted) throw new UnauthorizedError({ name: "Token user not found" });
|
||||
|
||||
if (token.organizationId) {
|
||||
const organization = await orgDAL.findById(token.organizationId);
|
||||
|
||||
if (organization.authEnforced) {
|
||||
const tokenAuthMode = token.authMethod;
|
||||
|
||||
if (![AuthMethod.AZURE_SAML, AuthMethod.OKTA_SAML, AuthMethod.JUMPCLOUD_SAML].includes(tokenAuthMode)) {
|
||||
throw new UnauthorizedError({ name: "Organization enforces SAML" });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { user, tokenVersionId: token.tokenVersionId, orgId: token.organizationId };
|
||||
};
|
||||
|
||||
|
@ -15,10 +15,10 @@ export const validateProviderAuthToken = (providerToken: string, username?: stri
|
||||
if (decodedToken.username !== username) throw new Error("Invalid auth credentials");
|
||||
|
||||
if (decodedToken.organizationId) {
|
||||
return { orgId: decodedToken.organizationId };
|
||||
return { orgId: decodedToken.organizationId, authMethod: decodedToken.authMethod };
|
||||
}
|
||||
|
||||
return {};
|
||||
return { authMethod: decodedToken.authMethod, orgId: null };
|
||||
};
|
||||
|
||||
export const validateSignUpAuthorization = (token: string, userId: string, validate = true) => {
|
||||
|
@ -17,7 +17,7 @@ import {
|
||||
TOauthLoginDTO,
|
||||
TVerifyMfaTokenDTO
|
||||
} from "./auth-login-type";
|
||||
import { AuthMethod, AuthTokenType } from "./auth-type";
|
||||
import { AuthMethod, AuthModeMfaJwtTokenPayload, AuthTokenType } from "./auth-type";
|
||||
|
||||
type TAuthLoginServiceFactoryDep = {
|
||||
userDAL: TUserDALFactory;
|
||||
@ -83,12 +83,14 @@ export const authLoginServiceFactory = ({ userDAL, tokenService, smtpService }:
|
||||
user,
|
||||
ip,
|
||||
userAgent,
|
||||
organizationId
|
||||
organizationId,
|
||||
authMethod
|
||||
}: {
|
||||
user: TUsers;
|
||||
ip: string;
|
||||
userAgent: string;
|
||||
organizationId?: string;
|
||||
authMethod: AuthMethod;
|
||||
}) => {
|
||||
const cfg = getConfig();
|
||||
await updateUserDeviceSession(user, ip, userAgent);
|
||||
@ -98,8 +100,10 @@ export const authLoginServiceFactory = ({ userDAL, tokenService, smtpService }:
|
||||
userId: user.id
|
||||
});
|
||||
if (!tokenSession) throw new Error("Failed to create token");
|
||||
|
||||
const accessToken = jwt.sign(
|
||||
{
|
||||
authMethod,
|
||||
authTokenType: AuthTokenType.ACCESS_TOKEN,
|
||||
userId: user.id,
|
||||
tokenVersionId: tokenSession.id,
|
||||
@ -112,6 +116,7 @@ export const authLoginServiceFactory = ({ userDAL, tokenService, smtpService }:
|
||||
|
||||
const refreshToken = jwt.sign(
|
||||
{
|
||||
authMethod,
|
||||
authTokenType: AuthTokenType.REFRESH_TOKEN,
|
||||
userId: user.id,
|
||||
tokenVersionId: tokenSession.id,
|
||||
@ -158,9 +163,9 @@ export const authLoginServiceFactory = ({ userDAL, tokenService, smtpService }:
|
||||
const loginExchangeClientProof = async ({
|
||||
email,
|
||||
clientProof,
|
||||
providerAuthToken,
|
||||
ip,
|
||||
userAgent
|
||||
userAgent,
|
||||
providerAuthToken
|
||||
}: TLoginClientProofDTO) => {
|
||||
const userEnc = await userDAL.findUserEncKeyByUsername({
|
||||
username: email
|
||||
@ -168,14 +173,14 @@ export const authLoginServiceFactory = ({ userDAL, tokenService, smtpService }:
|
||||
if (!userEnc) throw new Error("Failed to find user");
|
||||
const cfg = getConfig();
|
||||
|
||||
let organizationId;
|
||||
if (!userEnc.authMethods?.includes(AuthMethod.EMAIL)) {
|
||||
const { orgId } = validateProviderAuthToken(providerAuthToken as string, email);
|
||||
organizationId = orgId;
|
||||
} else if (providerAuthToken) {
|
||||
// SAML SSO
|
||||
const { orgId } = validateProviderAuthToken(providerAuthToken, email);
|
||||
organizationId = orgId;
|
||||
// let organizationId;
|
||||
|
||||
// let authMethod = (providerAuthToken as AuthMethod) || AuthMethod.EMAIL;
|
||||
|
||||
let authMethod = AuthMethod.EMAIL;
|
||||
|
||||
if (providerAuthToken) {
|
||||
authMethod = validateProviderAuthToken(providerAuthToken, email).authMethod;
|
||||
}
|
||||
|
||||
if (!userEnc.serverPrivateKey || !userEnc.clientPublicKey) throw new Error("Failed to authenticate. Try again?");
|
||||
@ -196,9 +201,9 @@ export const authLoginServiceFactory = ({ userDAL, tokenService, smtpService }:
|
||||
if (userEnc.isMfaEnabled && userEnc.email) {
|
||||
const mfaToken = jwt.sign(
|
||||
{
|
||||
authMethod,
|
||||
authTokenType: AuthTokenType.MFA_TOKEN,
|
||||
userId: userEnc.userId,
|
||||
organizationId
|
||||
userId: userEnc.userId
|
||||
},
|
||||
cfg.AUTH_SECRET,
|
||||
{
|
||||
@ -221,7 +226,7 @@ export const authLoginServiceFactory = ({ userDAL, tokenService, smtpService }:
|
||||
},
|
||||
ip,
|
||||
userAgent,
|
||||
organizationId
|
||||
authMethod
|
||||
});
|
||||
|
||||
return { token, isMfaEnabled: false, user: userEnc } as const;
|
||||
@ -250,6 +255,9 @@ export const authLoginServiceFactory = ({ userDAL, tokenService, smtpService }:
|
||||
userId,
|
||||
code: mfaToken
|
||||
});
|
||||
|
||||
const decodedToken = jwt.verify(mfaToken, getConfig().AUTH_SECRET) as AuthModeMfaJwtTokenPayload;
|
||||
|
||||
const userEnc = await userDAL.findUserEncKeyByUserId(userId);
|
||||
if (!userEnc) throw new Error("Failed to authenticate user");
|
||||
|
||||
@ -260,7 +268,8 @@ export const authLoginServiceFactory = ({ userDAL, tokenService, smtpService }:
|
||||
},
|
||||
ip,
|
||||
userAgent,
|
||||
organizationId: orgId
|
||||
organizationId: orgId,
|
||||
authMethod: decodedToken.authMethod
|
||||
});
|
||||
|
||||
return { token, user: userEnc };
|
||||
|
@ -174,6 +174,7 @@ export const authSignupServiceFactory = ({
|
||||
|
||||
const accessToken = jwt.sign(
|
||||
{
|
||||
authMethod: AuthMethod.EMAIL,
|
||||
authTokenType: AuthTokenType.ACCESS_TOKEN,
|
||||
userId: updateduser.info.id,
|
||||
tokenVersionId: tokenSession.id,
|
||||
@ -277,6 +278,7 @@ export const authSignupServiceFactory = ({
|
||||
|
||||
const accessToken = jwt.sign(
|
||||
{
|
||||
authMethod: AuthMethod.EMAIL,
|
||||
authTokenType: AuthTokenType.ACCESS_TOKEN,
|
||||
userId: updateduser.info.id,
|
||||
tokenVersionId: tokenSession.id,
|
||||
|
@ -40,6 +40,7 @@ export enum ActorType { // would extend to AWS, Azure, ...
|
||||
|
||||
export type AuthModeJwtTokenPayload = {
|
||||
authTokenType: AuthTokenType.ACCESS_TOKEN;
|
||||
authMethod: AuthMethod;
|
||||
userId: string;
|
||||
tokenVersionId: string;
|
||||
accessVersion: number;
|
||||
@ -48,12 +49,15 @@ export type AuthModeJwtTokenPayload = {
|
||||
|
||||
export type AuthModeMfaJwtTokenPayload = {
|
||||
authTokenType: AuthTokenType.MFA_TOKEN;
|
||||
authMethod: AuthMethod;
|
||||
userId: string;
|
||||
organizationId?: string;
|
||||
};
|
||||
|
||||
export type AuthModeRefreshJwtTokenPayload = {
|
||||
// authMode
|
||||
authTokenType: AuthTokenType.REFRESH_TOKEN;
|
||||
authMethod: AuthMethod;
|
||||
userId: string;
|
||||
tokenVersionId: string;
|
||||
refreshVersion: number;
|
||||
@ -63,6 +67,8 @@ export type AuthModeRefreshJwtTokenPayload = {
|
||||
export type AuthModeProviderJwtTokenPayload = {
|
||||
authTokenType: AuthTokenType.PROVIDER_TOKEN;
|
||||
username: string;
|
||||
authMethod: AuthMethod;
|
||||
email: string;
|
||||
organizationId?: string;
|
||||
};
|
||||
|
||||
|
@ -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 { TProjectDALFactory } from "../project/project-dal";
|
||||
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
||||
import { TUserDALFactory } from "../user/user-dal";
|
||||
import { TServiceTokenDALFactory } from "./service-token-dal";
|
||||
@ -24,6 +25,7 @@ type TServiceTokenServiceFactoryDep = {
|
||||
userDAL: TUserDALFactory;
|
||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||
projectEnvDAL: Pick<TProjectEnvDALFactory, "findBySlugs">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findById">;
|
||||
};
|
||||
|
||||
export type TServiceTokenServiceFactory = ReturnType<typeof serviceTokenServiceFactory>;
|
||||
@ -32,7 +34,8 @@ export const serviceTokenServiceFactory = ({
|
||||
serviceTokenDAL,
|
||||
userDAL,
|
||||
permissionService,
|
||||
projectEnvDAL
|
||||
projectEnvDAL,
|
||||
projectDAL
|
||||
}: TServiceTokenServiceFactoryDep) => {
|
||||
const createServiceToken = async ({
|
||||
iv,
|
||||
@ -132,6 +135,9 @@ export const serviceTokenServiceFactory = ({
|
||||
const serviceToken = await serviceTokenDAL.findById(TOKEN_IDENTIFIER);
|
||||
|
||||
if (!serviceToken) throw new UnauthorizedError();
|
||||
const project = await projectDAL.findById(serviceToken.projectId);
|
||||
|
||||
if (!project) throw new UnauthorizedError({ message: "Service token project not found" });
|
||||
|
||||
if (serviceToken.expiresAt && new Date(serviceToken.expiresAt) < new Date()) {
|
||||
await serviceTokenDAL.deleteById(serviceToken.id);
|
||||
@ -144,7 +150,7 @@ export const serviceTokenServiceFactory = ({
|
||||
lastUsed: new Date()
|
||||
});
|
||||
|
||||
return { ...serviceToken, lastUsed: updatedToken.lastUsed };
|
||||
return { ...serviceToken, lastUsed: updatedToken.lastUsed, orgId: project.orgId };
|
||||
};
|
||||
|
||||
return {
|
||||
|
@ -13,13 +13,13 @@ export const PermissionDeniedBanner = ({ containerClassName, className, children
|
||||
return (
|
||||
<div
|
||||
className={twMerge(
|
||||
"container h-full mx-auto flex justify-center items-center",
|
||||
"container mx-auto flex h-full items-center justify-center",
|
||||
containerClassName
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={twMerge(
|
||||
"rounded-md bg-mineshaft-800 text-bunker-300 p-16 flex space-x-12 items-end",
|
||||
"flex items-end space-x-12 rounded-md bg-mineshaft-800 p-16 text-bunker-300",
|
||||
className
|
||||
)}
|
||||
>
|
||||
@ -27,10 +27,11 @@ export const PermissionDeniedBanner = ({ containerClassName, className, children
|
||||
<FontAwesomeIcon icon={faLock} size="6x" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-4xl font-medium mb-2">Access Restricted</div>
|
||||
<div className="mb-2 text-4xl font-medium">Access Restricted</div>
|
||||
{children || (
|
||||
<div className="text-sm">
|
||||
Your role has limited permissions, please <br/> contact your administrator to gain access
|
||||
Your role has limited permissions, please <br /> contact your administrator to gain
|
||||
access
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
Reference in New Issue
Block a user