mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 21:27:10 +00:00
feat: added logic for jwt auth login
This commit is contained in:
@@ -94,6 +94,7 @@ export enum EventType {
|
|||||||
UPDATE_IDENTITY_OIDC_AUTH = "update-identity-oidc-auth",
|
UPDATE_IDENTITY_OIDC_AUTH = "update-identity-oidc-auth",
|
||||||
GET_IDENTITY_OIDC_AUTH = "get-identity-oidc-auth",
|
GET_IDENTITY_OIDC_AUTH = "get-identity-oidc-auth",
|
||||||
REVOKE_IDENTITY_OIDC_AUTH = "revoke-identity-oidc-auth",
|
REVOKE_IDENTITY_OIDC_AUTH = "revoke-identity-oidc-auth",
|
||||||
|
LOGIN_IDENTITY_JWT_AUTH = "login-identity-jwt-auth",
|
||||||
ADD_IDENTITY_JWT_AUTH = "add-identity-jwt-auth",
|
ADD_IDENTITY_JWT_AUTH = "add-identity-jwt-auth",
|
||||||
UPDATE_IDENTITY_JWT_AUTH = "update-identity-jwt-auth",
|
UPDATE_IDENTITY_JWT_AUTH = "update-identity-jwt-auth",
|
||||||
GET_IDENTITY_JWT_AUTH = "get-identity-jwt-auth",
|
GET_IDENTITY_JWT_AUTH = "get-identity-jwt-auth",
|
||||||
@@ -899,6 +900,15 @@ interface GetIdentityOidcAuthEvent {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface LoginIdentityJwtAuthEvent {
|
||||||
|
type: EventType.LOGIN_IDENTITY_JWT_AUTH;
|
||||||
|
metadata: {
|
||||||
|
identityId: string;
|
||||||
|
identityJwtAuthId: string;
|
||||||
|
identityAccessTokenId: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
interface AddIdentityJwtAuthEvent {
|
interface AddIdentityJwtAuthEvent {
|
||||||
type: EventType.ADD_IDENTITY_JWT_AUTH;
|
type: EventType.ADD_IDENTITY_JWT_AUTH;
|
||||||
metadata: {
|
metadata: {
|
||||||
@@ -1789,6 +1799,7 @@ export type Event =
|
|||||||
| DeleteIdentityOidcAuthEvent
|
| DeleteIdentityOidcAuthEvent
|
||||||
| UpdateIdentityOidcAuthEvent
|
| UpdateIdentityOidcAuthEvent
|
||||||
| GetIdentityOidcAuthEvent
|
| GetIdentityOidcAuthEvent
|
||||||
|
| LoginIdentityJwtAuthEvent
|
||||||
| AddIdentityJwtAuthEvent
|
| AddIdentityJwtAuthEvent
|
||||||
| UpdateIdentityJwtAuthEvent
|
| UpdateIdentityJwtAuthEvent
|
||||||
| GetIdentityJwtAuthEvent
|
| GetIdentityJwtAuthEvent
|
||||||
|
|||||||
@@ -22,6 +22,55 @@ const IdentityJwtAuthResponseSchema = IdentityJwtAuthsSchema.omit({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const registerIdentityJwtAuthRouter = async (server: FastifyZodProvider) => {
|
export const registerIdentityJwtAuthRouter = async (server: FastifyZodProvider) => {
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/jwt-auth/login",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
description: "Login with JWT Auth",
|
||||||
|
body: z.object({
|
||||||
|
identityId: z.string().trim().describe(JWT_AUTH.LOGIN.identityId),
|
||||||
|
jwt: z.string().trim()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
accessToken: z.string(),
|
||||||
|
expiresIn: z.coerce.number(),
|
||||||
|
accessTokenMaxTTL: z.coerce.number(),
|
||||||
|
tokenType: z.literal("Bearer")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handler: async (req) => {
|
||||||
|
const { identityJwtAuth, accessToken, identityAccessToken, identityMembershipOrg } =
|
||||||
|
await server.services.identityJwtAuth.login({
|
||||||
|
identityId: req.body.identityId,
|
||||||
|
jwt: req.body.jwt
|
||||||
|
});
|
||||||
|
|
||||||
|
await server.services.auditLog.createAuditLog({
|
||||||
|
...req.auditLogInfo,
|
||||||
|
orgId: identityMembershipOrg?.orgId,
|
||||||
|
event: {
|
||||||
|
type: EventType.LOGIN_IDENTITY_JWT_AUTH,
|
||||||
|
metadata: {
|
||||||
|
identityId: identityJwtAuth.identityId,
|
||||||
|
identityAccessTokenId: identityAccessToken.id,
|
||||||
|
identityJwtAuthId: identityJwtAuth.id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
accessToken,
|
||||||
|
tokenType: "Bearer" as const,
|
||||||
|
expiresIn: identityJwtAuth.accessTokenTTL,
|
||||||
|
accessTokenMaxTTL: identityJwtAuth.accessTokenMaxTTL
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: "/jwt-auth/identities/:identityId",
|
url: "/jwt-auth/identities/:identityId",
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import picomatch from "picomatch";
|
||||||
|
|
||||||
|
export const doesFieldValueMatchJwtPolicy = (fieldValue: string, policyValue: string) =>
|
||||||
|
policyValue === fieldValue || picomatch.isMatch(fieldValue, policyValue);
|
||||||
@@ -1,20 +1,33 @@
|
|||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
import https from "https";
|
||||||
|
import jwt, { JsonWebTokenError } from "jsonwebtoken";
|
||||||
|
import { JwksClient } from "jwks-rsa";
|
||||||
|
|
||||||
import { IdentityAuthMethod, TIdentityJwtAuthsUpdate } from "@app/db/schemas";
|
import { IdentityAuthMethod, TIdentityJwtAuthsUpdate } from "@app/db/schemas";
|
||||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
import { isAtLeastAsPrivileged } from "@app/lib/casl";
|
||||||
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
|
import { BadRequestError, ForbiddenRequestError, NotFoundError, UnauthorizedError } from "@app/lib/errors";
|
||||||
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
||||||
|
|
||||||
import { ActorType } from "../auth/auth-type";
|
import { ActorType, AuthTokenType } from "../auth/auth-type";
|
||||||
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
|
import { TIdentityOrgDALFactory } from "../identity/identity-org-dal";
|
||||||
import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal";
|
import { TIdentityAccessTokenDALFactory } from "../identity-access-token/identity-access-token-dal";
|
||||||
|
import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identity-access-token-types";
|
||||||
import { TKmsServiceFactory } from "../kms/kms-service";
|
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||||
import { KmsDataKey } from "../kms/kms-types";
|
import { KmsDataKey } from "../kms/kms-types";
|
||||||
import { TIdentityJwtAuthDALFactory } from "./identity-jwt-auth-dal";
|
import { TIdentityJwtAuthDALFactory } from "./identity-jwt-auth-dal";
|
||||||
import { TAttachJwtAuthDTO, TGetJwtAuthDTO, TRevokeJwtAuthDTO, TUpdateJwtAuthDTO } from "./identity-jwt-auth-types";
|
import { doesFieldValueMatchJwtPolicy } from "./identity-jwt-auth-fns";
|
||||||
|
import {
|
||||||
|
JwtConfigurationType,
|
||||||
|
TAttachJwtAuthDTO,
|
||||||
|
TGetJwtAuthDTO,
|
||||||
|
TLoginJwtAuthDTO,
|
||||||
|
TRevokeJwtAuthDTO,
|
||||||
|
TUpdateJwtAuthDTO
|
||||||
|
} from "./identity-jwt-auth-types";
|
||||||
|
|
||||||
type TIdentityJwtAuthServiceFactoryDep = {
|
type TIdentityJwtAuthServiceFactoryDep = {
|
||||||
identityJwtAuthDAL: TIdentityJwtAuthDALFactory;
|
identityJwtAuthDAL: TIdentityJwtAuthDALFactory;
|
||||||
@@ -35,6 +48,162 @@ export const identityJwtAuthServiceFactory = ({
|
|||||||
identityAccessTokenDAL,
|
identityAccessTokenDAL,
|
||||||
kmsService
|
kmsService
|
||||||
}: TIdentityJwtAuthServiceFactoryDep) => {
|
}: TIdentityJwtAuthServiceFactoryDep) => {
|
||||||
|
const login = async ({ identityId, jwt: jwtValue }: TLoginJwtAuthDTO) => {
|
||||||
|
const identityJwtAuth = await identityJwtAuthDAL.findOne({ identityId });
|
||||||
|
if (!identityJwtAuth) {
|
||||||
|
throw new NotFoundError({ message: "JWT auth method not found for identity, did you configure JWT auth?" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({
|
||||||
|
identityId: identityJwtAuth.identityId
|
||||||
|
});
|
||||||
|
if (!identityMembershipOrg) {
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: `Identity organization membership for identity with ID '${identityJwtAuth.identityId}' not found`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { decryptor: orgDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
|
type: KmsDataKey.Organization,
|
||||||
|
orgId: identityMembershipOrg.orgId
|
||||||
|
});
|
||||||
|
|
||||||
|
const decodedToken = jwt.decode(jwtValue, { complete: true });
|
||||||
|
if (!decodedToken) {
|
||||||
|
throw new UnauthorizedError({
|
||||||
|
message: "Invalid JWT"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let tokenData: Record<string, string> = {};
|
||||||
|
|
||||||
|
if (identityJwtAuth.configurationType === JwtConfigurationType.JWKS) {
|
||||||
|
const decryptedJwksCaCert = orgDataKeyDecryptor({
|
||||||
|
cipherTextBlob: identityJwtAuth.encryptedJwksCaCert
|
||||||
|
}).toString();
|
||||||
|
const requestAgent = new https.Agent({ ca: decryptedJwksCaCert, rejectUnauthorized: !!decryptedJwksCaCert });
|
||||||
|
const client = new JwksClient({
|
||||||
|
jwksUri: identityJwtAuth.jwksUrl,
|
||||||
|
requestAgent
|
||||||
|
});
|
||||||
|
|
||||||
|
const { kid } = decodedToken.header;
|
||||||
|
const jwtSigningKey = await client.getSigningKey(kid);
|
||||||
|
|
||||||
|
try {
|
||||||
|
tokenData = jwt.verify(jwtValue, jwtSigningKey.getPublicKey()) as Record<string, string>;
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof jwt.JsonWebTokenError) {
|
||||||
|
throw new UnauthorizedError({
|
||||||
|
message: `Access denied: ${error.message}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const decryptedPublicKeys = orgDataKeyDecryptor({ cipherTextBlob: identityJwtAuth.encryptedPublicKeys })
|
||||||
|
.toString()
|
||||||
|
.split(",");
|
||||||
|
|
||||||
|
const errors: string[] = [];
|
||||||
|
let isMatchAnyKey = false;
|
||||||
|
for (const publicKey of decryptedPublicKeys) {
|
||||||
|
try {
|
||||||
|
tokenData = jwt.verify(jwtValue, publicKey) as Record<string, string>;
|
||||||
|
isMatchAnyKey = true;
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof JsonWebTokenError) {
|
||||||
|
errors.push(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isMatchAnyKey) {
|
||||||
|
throw new UnauthorizedError({
|
||||||
|
message: `Access denied: JWT verification failed with all keys. Errors - ${errors.join("; ")}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (identityJwtAuth.boundIssuer) {
|
||||||
|
if (!doesFieldValueMatchJwtPolicy(tokenData.iss, identityJwtAuth.boundIssuer)) {
|
||||||
|
throw new ForbiddenRequestError({
|
||||||
|
message: "Access denied: issuer mismatch."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (identityJwtAuth.boundSubject) {
|
||||||
|
if (!doesFieldValueMatchJwtPolicy(tokenData.sub, identityJwtAuth.boundSubject)) {
|
||||||
|
throw new ForbiddenRequestError({
|
||||||
|
message: "Access denied: subject not allowed."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (identityJwtAuth.boundAudiences) {
|
||||||
|
if (
|
||||||
|
!identityJwtAuth.boundAudiences
|
||||||
|
.split(", ")
|
||||||
|
.some((policyValue) => doesFieldValueMatchJwtPolicy(tokenData.aud, policyValue))
|
||||||
|
) {
|
||||||
|
throw new UnauthorizedError({
|
||||||
|
message: "Access denied: audience not allowed."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (identityJwtAuth.boundClaims) {
|
||||||
|
Object.keys(identityJwtAuth.boundClaims).forEach((claimKey) => {
|
||||||
|
const claimValue = (identityJwtAuth.boundClaims as Record<string, string>)[claimKey];
|
||||||
|
// handle both single and multi-valued claims
|
||||||
|
if (
|
||||||
|
!claimValue.split(", ").some((claimEntry) => doesFieldValueMatchJwtPolicy(tokenData[claimKey], claimEntry))
|
||||||
|
) {
|
||||||
|
throw new UnauthorizedError({
|
||||||
|
message: "Access denied: claim mismatch."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const identityAccessToken = await identityJwtAuthDAL.transaction(async (tx) => {
|
||||||
|
const newToken = await identityAccessTokenDAL.create(
|
||||||
|
{
|
||||||
|
identityId: identityJwtAuth.identityId,
|
||||||
|
isAccessTokenRevoked: false,
|
||||||
|
accessTokenTTL: identityJwtAuth.accessTokenTTL,
|
||||||
|
accessTokenMaxTTL: identityJwtAuth.accessTokenMaxTTL,
|
||||||
|
accessTokenNumUses: 0,
|
||||||
|
accessTokenNumUsesLimit: identityJwtAuth.accessTokenNumUsesLimit,
|
||||||
|
authMethod: IdentityAuthMethod.JWT_AUTH
|
||||||
|
},
|
||||||
|
tx
|
||||||
|
);
|
||||||
|
|
||||||
|
return newToken;
|
||||||
|
});
|
||||||
|
|
||||||
|
const appCfg = getConfig();
|
||||||
|
const accessToken = jwt.sign(
|
||||||
|
{
|
||||||
|
identityId: identityJwtAuth.identityId,
|
||||||
|
identityAccessTokenId: identityAccessToken.id,
|
||||||
|
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
|
||||||
|
} as TIdentityAccessTokenJwtPayload,
|
||||||
|
appCfg.AUTH_SECRET,
|
||||||
|
{
|
||||||
|
expiresIn:
|
||||||
|
Number(identityAccessToken.accessTokenMaxTTL) === 0
|
||||||
|
? undefined
|
||||||
|
: Number(identityAccessToken.accessTokenMaxTTL)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return { accessToken, identityJwtAuth, identityAccessToken, identityMembershipOrg };
|
||||||
|
};
|
||||||
|
|
||||||
const attachJwtAuth = async ({
|
const attachJwtAuth = async ({
|
||||||
identityId,
|
identityId,
|
||||||
configurationType,
|
configurationType,
|
||||||
@@ -337,6 +506,7 @@ export const identityJwtAuthServiceFactory = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
login,
|
||||||
attachJwtAuth,
|
attachJwtAuth,
|
||||||
updateJwtAuth,
|
updateJwtAuth,
|
||||||
getJwtAuth,
|
getJwtAuth,
|
||||||
|
|||||||
@@ -44,3 +44,8 @@ export type TGetJwtAuthDTO = {
|
|||||||
export type TRevokeJwtAuthDTO = {
|
export type TRevokeJwtAuthDTO = {
|
||||||
identityId: string;
|
identityId: string;
|
||||||
} & Omit<TProjectPermission, "projectId">;
|
} & Omit<TProjectPermission, "projectId">;
|
||||||
|
|
||||||
|
export type TLoginJwtAuthDTO = {
|
||||||
|
identityId: string;
|
||||||
|
jwt: string;
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user