diff --git a/backend/src/controllers/v1/authController.ts b/backend/src/controllers/v1/authController.ts index 4ae5a79ff..363d9eee0 100644 --- a/backend/src/controllers/v1/authController.ts +++ b/backend/src/controllers/v1/authController.ts @@ -27,8 +27,9 @@ declare module "jsonwebtoken" { } export interface MachineAccessTokenJwtPayload extends jwt.JwtPayload { _id: string; + clientSecretId: string; + machineAccessTokenId: string; authTokenType: string; - tokenVersion: number; } } diff --git a/backend/src/ee/controllers/v1/machineIdentitiesController.ts b/backend/src/ee/controllers/v1/machineIdentitiesController.ts index 904ab2734..eccc4246c 100644 --- a/backend/src/ee/controllers/v1/machineIdentitiesController.ts +++ b/backend/src/ee/controllers/v1/machineIdentitiesController.ts @@ -1,3 +1,4 @@ +import jwt from "jsonwebtoken"; import bcrypt from "bcrypt"; import crypto from "crypto"; import { Request, Response } from "express"; @@ -6,6 +7,7 @@ import { IMachineIdentity, IMachineIdentityClientSecret, IMachineIdentityTrustedIp, + IdentityAccessToken, MachineIdentity, MachineIdentityClientSecret, MachineMembership, @@ -47,12 +49,14 @@ import { getUserAgentType } from "../../../utils/posthog"; const packageClientSecretData = (machineIdentityClientSecret: IMachineIdentityClientSecret) => ({ _id: machineIdentityClientSecret._id, machineIdentity: machineIdentityClientSecret.machineIdentity, - isActive: machineIdentityClientSecret.isActive, + isClientSecretRevoked: machineIdentityClientSecret.isClientSecretRevoked, description: machineIdentityClientSecret.description, clientSecretPrefix: machineIdentityClientSecret.clientSecretPrefix, clientSecretNumUses: machineIdentityClientSecret.clientSecretNumUses, clientSecretNumUsesLimit: machineIdentityClientSecret.clientSecretNumUsesLimit, - clientSecretTTL: machineIdentityClientSecret.clientSecretTTL + clientSecretTTL: machineIdentityClientSecret.clientSecretTTL, + createdAt: machineIdentityClientSecret.createdAt, + updatedAt: machineIdentityClientSecret.updatedAt }); /** @@ -65,7 +69,7 @@ export const getMIClientSecrets = async (req: Request, res: Response) => { params: { machineId } - } = await validateRequest(reqValidator.GetClientSecretsV3, req); + } = await validateRequest(reqValidator.GetClientSecretsV1, req); const machineMembershipOrg = await MachineMembershipOrg.findOne({ machineIdentity: new Types.ObjectId(machineId) @@ -97,8 +101,7 @@ export const getMIClientSecrets = async (req: Request, res: Response) => { const clientSecretData = await MachineIdentityClientSecret .find({ - machineIdentity: machineMembershipOrg.machineIdentity, - isActive: true + machineIdentity: machineMembershipOrg.machineIdentity }) .sort({ createdAt: -1 }) .limit(5); @@ -108,8 +111,7 @@ export const getMIClientSecrets = async (req: Request, res: Response) => { { type: EventType.GET_MACHINE_IDENTITY_CLIENT_SECRETS, metadata: { - machineId: machineMembershipOrg.machineIdentity._id.toString(), - clientId: machineMembershipOrg.machineIdentity.clientId, + machineId: machineMembershipOrg.machineIdentity._id.toString() } }, { @@ -137,7 +139,7 @@ export const createMIClientSecret = async (req: Request, res: Response) => { ttl, numUsesLimit } - } = await validateRequest(reqValidator.CreateClientSecretV3, req); + } = await validateRequest(reqValidator.CreateClientSecretV1, req); const machineMembershipOrg = await MachineMembershipOrg.findOne({ machineIdentity: new Types.ObjectId(machineId) @@ -173,14 +175,13 @@ export const createMIClientSecret = async (req: Request, res: Response) => { const machineIdentityClientSecret = await new MachineIdentityClientSecret({ machineIdentity: machineMembershipOrg.machineIdentity, - isActive: true, description, clientSecretPrefix: clientSecret.slice(0, 4), clientSecretHash, clientSecretNumUses: 0, clientSecretNumUsesLimit: numUsesLimit, clientSecretTTL: ttl, - accessTokenVersion: 1, + isClientSecretRevoked: false }).save(); await EEAuditLogService.createAuditLog( @@ -189,7 +190,6 @@ export const createMIClientSecret = async (req: Request, res: Response) => { type: EventType.CREATE_MACHINE_IDENTITY_CLIENT_SECRET, metadata: { machineId: machineMembershipOrg.machineIdentity._id.toString(), - clientId: machineMembershipOrg.machineIdentity.clientId, clientSecretId: machineIdentityClientSecret._id.toString() } }, @@ -215,7 +215,7 @@ export const deleteMIClientSecret = async (req: Request, res: Response) => { machineId, clientSecretId } - } = await validateRequest(reqValidator.DeleteClientSecretV3, req); + } = await validateRequest(reqValidator.DeleteClientSecretV1, req); const machineMembershipOrg = await MachineMembershipOrg .findOne({ @@ -250,20 +250,27 @@ export const deleteMIClientSecret = async (req: Request, res: Response) => { message: "Failed to delete client secrets for more privileged MI" }); - const machineIdentityClientSecret = await MachineIdentityClientSecret.findOneAndDelete({ - _id: clientSecretId, - machineIdentity: machineId - }); + const machineIdentityClientSecret = await MachineIdentityClientSecret.findOneAndUpdate( + { + _id: clientSecretId, + machineIdentity: machineId + }, + { + isClientSecretRevoked: true + }, + { + new: true + } + ); if (!machineIdentityClientSecret) throw ResourceNotFoundError(); await EEAuditLogService.createAuditLog( req.authData, { - type: EventType.DELETE_MACHINE_IDENTITY_CLIENT_SECRET, + type: EventType.REVOKE_MACHINE_IDENTITY_CLIENT_SECRET, metadata: { machineId: machineMembershipOrg.machineIdentity._id.toString(), - clientId: machineMembershipOrg.machineIdentity.clientId, clientSecretId: clientSecretId } }, @@ -289,11 +296,10 @@ export const loginMI = async (req: Request, res: Response) => { clientId, clientSecret } - } = await validateRequest(reqValidator.LoginMachineIdentityV3, req); + } = await validateRequest(reqValidator.LoginMachineIdentityV1, req); const machineIdentity = await MachineIdentity.findOne({ - clientId, - isActive: true + clientId }); if (!machineIdentity) throw UnauthorizedRequestError(); @@ -305,7 +311,7 @@ export const loginMI = async (req: Request, res: Response) => { const clientSecretData = await MachineIdentityClientSecret.find({ machineIdentity: machineIdentity._id, - isActive: true + isClientSecretRevoked: false }); let validatedClientSecretDatum: IMachineIdentityClientSecret | undefined; @@ -340,7 +346,7 @@ export const loginMI = async (req: Request, res: Response) => { await MachineIdentityClientSecret.findByIdAndUpdate( validatedClientSecretDatum._id, { - isActive: false + isClientSecretRevoked: true } ); @@ -350,13 +356,13 @@ export const loginMI = async (req: Request, res: Response) => { } } - if (clientSecretNumUses > 0 && clientSecretNumUses === clientSecretNumUsesLimit) { + if (clientSecretNumUsesLimit > 0 && clientSecretNumUses === clientSecretNumUsesLimit) { // number of times client secret can be used for // a login operation reached await MachineIdentityClientSecret.findByIdAndUpdate( validatedClientSecretDatum._id, { - isActive: false + isClientSecretRevoked: true }, { new: true @@ -372,20 +378,31 @@ export const loginMI = async (req: Request, res: Response) => { await MachineIdentityClientSecret.findByIdAndUpdate( validatedClientSecretDatum._id, { + clientSecretLastUsedAt: new Date(), $inc: { clientSecretNumUses: 1 } }, { new: true } ); + + const identityAccessToken = await new IdentityAccessToken({ + machineIdentity: machineIdentity._id, + machineIdentityClientSecret: validatedClientSecretDatum._id, + accessTokenNumUses: 0, + accessTokenNumUsesLimit: machineIdentity.accessTokenNumUsesLimit, + accessTokenTTL: machineIdentity.accessTokenTTL, + accessTokenMaxTTL: machineIdentity.accessTokenMaxTTL, + isAccessTokenRevoked: false + }).save(); // token version const accessToken = createToken({ payload: { machineId: machineIdentity._id.toString(), - clientSecretDataId: validatedClientSecretDatum._id.toString(), - authTokenType: AuthTokenType.MACHINE_ACCESS_TOKEN, - tokenVersion: validatedClientSecretDatum.accessTokenVersion + clientSecretId: validatedClientSecretDatum._id.toString(), + identityAccessTokenId: identityAccessToken._id.toString(), + authTokenType: AuthTokenType.MACHINE_ACCESS_TOKEN }, expiresIn: machineIdentity.accessTokenTTL, secret: await getAuthSecret() @@ -411,8 +428,9 @@ export const loginMI = async (req: Request, res: Response) => { type: EventType.LOGIN_MACHINE_IDENTITY, metadata: { machineId: machineIdentity._id.toString(), - clientId, - clientSecretId: validatedClientSecretDatum._id.toString() + machineAccessTokenId: identityAccessToken._id.toString(), + clientSecretId: validatedClientSecretDatum._id.toString(), + identityAccessTokenId: identityAccessToken._id.toString() } }, { @@ -427,6 +445,79 @@ export const loginMI = async (req: Request, res: Response) => { }); } +/** + * Renews an access token by its TTL + * @param req + * @param res + */ +export const renewAccessToken = async (req: Request, res: Response) => { + const { + body: { + accessToken + } + } = await validateRequest(reqValidator.RenewAccessTokenV1, req); + + const decodedToken = ( + jwt.verify(accessToken, await getAuthSecret()) + ); + + if (decodedToken.authTokenType !== AuthTokenType.MACHINE_ACCESS_TOKEN) throw UnauthorizedRequestError(); + + const machineIdentityAccessToken = await IdentityAccessToken.findOne({ + _id: decodedToken.identityAccessTokenId, + isAccessTokenRevoked: false + }); + + if (!machineIdentityAccessToken) throw UnauthorizedRequestError(); + + const { + accessTokenTTL, + accessTokenLastRenewedAt, + accessTokenMaxTTL, + createdAt: accessTokenCreatedAt + } = machineIdentityAccessToken; + + if (accessTokenTTL === accessTokenMaxTTL) throw UnauthorizedRequestError({ + message: "Failed to renew non-renewable access token" + }); + + if (accessTokenTTL > 0) { + const currentDate = new Date(); + if (accessTokenLastRenewedAt) { + // access token has been renewed + const accessTokenRenewed = new Date(accessTokenLastRenewedAt); + const ttlInMilliseconds = accessTokenTTL * 1000; + const expirationTime = new Date(accessTokenRenewed.getTime() + ttlInMilliseconds); + + if (currentDate > expirationTime) throw UnauthorizedRequestError({ + message: "Failed to renew MI access token due to TTL expiration" + }); + } else { + // access token has never been renewed + const accessTokenCreated = new Date(accessTokenCreatedAt); + const ttlInMilliseconds = accessTokenTTL * 1000; + const expirationTime = new Date(accessTokenCreated.getTime() + ttlInMilliseconds); + + if (currentDate > expirationTime) throw UnauthorizedRequestError({ + message: "Failed to renew MI access token due to TTL expiration" + }); + } + } + + await IdentityAccessToken.findByIdAndUpdate( + machineIdentityAccessToken._id, + { + accessTokenLastRenewedAt: new Date() + } + ); + + return res.status(200).send({ + accessToken, + expiresIn: machineIdentityAccessToken.accessTokenTTL, + tokenType: "Bearer" + }); +} + /** * Create machine identity * @param req @@ -442,8 +533,10 @@ export const createMachineIdentity = async (req: Request, res: Response) => { clientSecretTrustedIps, accessTokenTrustedIps, accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit } - } = await validateRequest(reqValidator.CreateMachineIdentityV3, req); + } = await validateRequest(reqValidator.CreateMachineIdentityV1, req); const { permission } = await getAuthDataOrgPermissions({ authData: req.authData, @@ -509,14 +602,14 @@ export const createMachineIdentity = async (req: Request, res: Response) => { return extractIPDetails(accessTokenTrustedIp.ipAddress); }); - const isActive = true; const machineIdentity = await new MachineIdentity({ clientId: crypto.randomUUID(), name, organization: new Types.ObjectId(organizationId), - isActive, accessTokenTTL, - accessTokenUsageCount: 0, + accessTokenMaxTTL, + accessTokenNumUses: 0, + accessTokenNumUsesLimit, clientSecretTrustedIps: reformattedClientSecretTrustedIps, accessTokenTrustedIps: reformattedAccessTokenTrustedIps, }).save(); @@ -534,7 +627,6 @@ export const createMachineIdentity = async (req: Request, res: Response) => { type: EventType.CREATE_MACHINE_IDENTITY, metadata: { name, - isActive, role, clientSecretTrustedIps: reformattedClientSecretTrustedIps as Array, accessTokenTrustedIps: reformattedAccessTokenTrustedIps as Array @@ -564,9 +656,10 @@ export const updateMachineIdentity = async (req: Request, res: Response) => { role, clientSecretTrustedIps, accessTokenTrustedIps, - accessTokenTTL + accessTokenTTL, + accessTokenNumUsesLimit } - } = await validateRequest(reqValidator.UpdateMachineIdentityV3, req); + } = await validateRequest(reqValidator.UpdateMachineIdentityV1, req); const machineMembershipOrg = await MachineMembershipOrg .findOne({ @@ -666,7 +759,8 @@ export const updateMachineIdentity = async (req: Request, res: Response) => { name, clientSecretTrustedIps: reformattedClientSecretTrustedIps, accessTokenTrustedIps: reformattedAccessTokenTrustedIps, - accessTokenTTL + accessTokenTTL, + accessTokenNumUsesLimit }, { new: true @@ -727,7 +821,7 @@ export const updateMachineIdentity = async (req: Request, res: Response) => { export const deleteMachineIdentity = async (req: Request, res: Response) => { const { params: { machineId } - } = await validateRequest(reqValidator.DeleteMachineIdentityV3, req); + } = await validateRequest(reqValidator.DeleteMachineIdentityV1, req); const machineMembershipOrg = await MachineMembershipOrg .findOne({ @@ -775,9 +869,19 @@ export const deleteMachineIdentity = async (req: Request, res: Response) => { machineIdentity: machineMembershipOrg.machineIdentity }); + const machineIdentityClientSecretIds = await MachineIdentityClientSecret.distinct("_id", { + machineIdentity: machineMembershipOrg.machineIdentity + }); + await MachineIdentityClientSecret.deleteMany({ machineIdentity: machineMembershipOrg.machineIdentity }); + + await IdentityAccessToken.deleteMany({ + machineIdentityClientSecret: { + $in: machineIdentityClientSecretIds + } + }); await EEAuditLogService.createAuditLog( req.authData, @@ -785,7 +889,6 @@ export const deleteMachineIdentity = async (req: Request, res: Response) => { type: EventType.DELETE_MACHINE_IDENTITY, metadata: { name: machineIdentity.name, - isActive: machineIdentity.isActive, role: machineMembershipOrg.role, clientSecretTrustedIps: machineIdentity.clientSecretTrustedIps as Array, accessTokenTrustedIps: machineIdentity.accessTokenTrustedIps as Array, diff --git a/backend/src/ee/models/auditLog/enums.ts b/backend/src/ee/models/auditLog/enums.ts index 14d67f5b9..1710d6357 100644 --- a/backend/src/ee/models/auditLog/enums.ts +++ b/backend/src/ee/models/auditLog/enums.ts @@ -1,7 +1,7 @@ export enum ActorType { // would extend to AWS, Azure, ... - USER = "user", + USER = "user", // userIdentity SERVICE = "service", - MACHINE = "machine" + MACHINE = "machine" // machineIdentity } export enum UserAgentType { @@ -36,7 +36,7 @@ export enum EventType { DELETE_MACHINE_IDENTITY = "delete-machine-identity", LOGIN_MACHINE_IDENTITY = "login-machine-identity", CREATE_MACHINE_IDENTITY_CLIENT_SECRET = "create-machine-identity-secret", - DELETE_MACHINE_IDENTITY_CLIENT_SECRET = "delete-machine-identity-secret", + REVOKE_MACHINE_IDENTITY_CLIENT_SECRET = "revoke-machine-identity-secret", GET_MACHINE_IDENTITY_CLIENT_SECRETS = "get-machine-identity-secrets", CREATE_ENVIRONMENT = "create-environment", UPDATE_ENVIRONMENT = "update-environment", diff --git a/backend/src/ee/models/auditLog/types.ts b/backend/src/ee/models/auditLog/types.ts index e03bc1f7a..049b95d54 100644 --- a/backend/src/ee/models/auditLog/types.ts +++ b/backend/src/ee/models/auditLog/types.ts @@ -225,13 +225,10 @@ interface DeleteServiceTokenEvent { }; } -// TODO: review all logging for MIs including params etc. - interface CreateMachineIdentityEvent { type: EventType.CREATE_MACHINE_IDENTITY; metadata: { name: string; - isActive: boolean; role: string; clientSecretTrustedIps: Array; accessTokenTrustedIps: Array; @@ -252,7 +249,6 @@ interface DeleteMachineIdentityEvent { type: EventType.DELETE_MACHINE_IDENTITY; metadata: { name: string; - isActive: boolean; role: string; clientSecretTrustedIps: Array; accessTokenTrustedIps: Array; @@ -263,8 +259,9 @@ interface LoginMachineIdentityEvent { type: EventType.LOGIN_MACHINE_IDENTITY ; metadata: { machineId: string; - clientId: string; + machineAccessTokenId: string; clientSecretId: string; + identityAccessTokenId: string; }; } @@ -272,16 +269,14 @@ interface CreateMachineIdentitySecretEvent { type: EventType.CREATE_MACHINE_IDENTITY_CLIENT_SECRET ; metadata: { machineId: string; - clientId: string; clientSecretId: string; }; } interface DeleteMachineIdentitySecretEvent { - type: EventType.DELETE_MACHINE_IDENTITY_CLIENT_SECRET ; + type: EventType.REVOKE_MACHINE_IDENTITY_CLIENT_SECRET ; metadata: { machineId: string; - clientId: string; clientSecretId: string; }; } @@ -290,7 +285,6 @@ interface GetMachineIdentitySecretsEvent { type: EventType.GET_MACHINE_IDENTITY_CLIENT_SECRETS ; metadata: { machineId: string; - clientId: string; }; } diff --git a/backend/src/ee/routes/v1/machineIdentities.ts b/backend/src/ee/routes/v1/machineIdentities.ts index f7fa8c586..f1805a115 100644 --- a/backend/src/ee/routes/v1/machineIdentities.ts +++ b/backend/src/ee/routes/v1/machineIdentities.ts @@ -33,6 +33,12 @@ router.post( machineIdentitiesController.loginMI ); +// note: currently this is machine-identity specific +router.post( + "/access-token/renew", + machineIdentitiesController.renewAccessToken +); + router.post( "/", requireAuth({ diff --git a/backend/src/models/machineIdentityAccessToken.ts b/backend/src/models/identityAccessToken.ts similarity index 52% rename from backend/src/models/machineIdentityAccessToken.ts rename to backend/src/models/identityAccessToken.ts index 5681235f5..e75fdaf2f 100644 --- a/backend/src/models/machineIdentityAccessToken.ts +++ b/backend/src/models/identityAccessToken.ts @@ -1,33 +1,37 @@ import { Document, Schema, Types, model } from "mongoose"; -import { boolean } from "zod"; -export interface IMachineIdentityAccessToken extends Document { +export interface IIdentityAccessToken extends Document { _id: Types.ObjectId; - machineIdentityClientSecret: Types.ObjectId; - isActive: boolean; - accessTokenLastUsed?: Date; + machineIdentity?: Types.ObjectId; + machineIdentityClientSecret?: Types.ObjectId; + accessTokenLastUsedAt?: Date; + accessTokenLastRenewedAt?: Date; accessTokenNumUses: number; accessTokenNumUsesLimit: number; accessTokenTTL: number; - accessTokenVersion: number; - renewable: boolean; + accessTokenMaxTTL: number; + isAccessTokenRevoked: boolean; updatedAt: Date; createdAt: Date; } -const machineIdentityAccessTokenSchema = new Schema( +const identityAccessTokenSchema = new Schema( { + machineIdentity: { + type: Schema.Types.ObjectId, + ref: "MachineIdentity", + required: false + }, machineIdentityClientSecret: { type: Schema.Types.ObjectId, ref: "MachineIdentityClientSecret", - required: true + required: false }, - isActive: { - type: Boolean, - default: true, - required: true + accessTokenLastUsedAt: { + type: Date, + required: false }, - accessTokenLastUsed: { + accessTokenLastRenewedAt: { type: Date, required: false }, @@ -43,18 +47,21 @@ const machineIdentityAccessTokenSchema = new Schema( default: 0, // default: used as many times as needed required: true }, - accessTokenTTL: { + accessTokenTTL: { // seconds + // incremental lifetime type: Number, - default: 0, // default: does not expire + default: 7200, required: true }, - renewable: { - type: boolean, - default: false, // no refresh mechanism yet - }, - accessTokenVersion: { + accessTokenMaxTTL: { // seconds + // max lifetime type: Number, - default: 1, + default: 7200, + required: true + }, + isAccessTokenRevoked: { + type: Boolean, + default: false, required: true }, }, @@ -63,8 +70,4 @@ const machineIdentityAccessTokenSchema = new Schema( } ); -machineIdentityAccessTokenSchema.index( - { machineIdentityClientSecret: 1, isActive: 1 } -) - -export const MachineIdentityClientSecret = model("MachineIdentityAccessToken", machineIdentityAccessTokenSchema); \ No newline at end of file +export const IdentityAccessToken = model("IdentityAccessToken", identityAccessTokenSchema); \ No newline at end of file diff --git a/backend/src/models/index.ts b/backend/src/models/index.ts index b0f2b7c94..e7a66114d 100644 --- a/backend/src/models/index.ts +++ b/backend/src/models/index.ts @@ -22,6 +22,7 @@ export * from "./workspace"; export * from "./serviceTokenData"; // TODO: deprecate export * from "./machineIdentity"; export * from "./machineIdentityClientSecret"; +export * from "./identityAccessToken"; export * from "./machineMembershipOrg"; export * from "./machineMembership"; export * from "./apiKeyData"; // TODO: deprecate diff --git a/backend/src/models/machineIdentity.ts b/backend/src/models/machineIdentity.ts index 81a4bf20e..2a579d5ca 100644 --- a/backend/src/models/machineIdentity.ts +++ b/backend/src/models/machineIdentity.ts @@ -7,17 +7,14 @@ export interface IMachineIdentityTrustedIp { prefix: number; } -// TODO: rename to AppClient - export interface IMachineIdentity extends Document { _id: Types.ObjectId; clientId: string; name: string; organization: Types.ObjectId; - isActive: boolean; accessTokenTTL: number; - accessTokenLastUsed?: Date; - accessTokenUsageCount: number; + accessTokenMaxTTL: number; + accessTokenNumUsesLimit: number; clientSecretTrustedIps: Array; accessTokenTrustedIps: Array; } @@ -37,23 +34,22 @@ const machineIdentitySchema = new Schema( ref: "Organization", required: true }, - isActive: { - type: Boolean, - default: true, - required: true - }, accessTokenTTL: { // seconds + // incremental lifetime type: Number, default: 7200, required: true }, - accessTokenLastUsed: { - type: Date, - required: false - }, - accessTokenUsageCount: { + accessTokenMaxTTL: { // seconds + // max lifetime type: Number, - default: 0, + default: 7200, + required: true + }, + accessTokenNumUsesLimit: { + // number of times access token can be used for + type: Number, + default: 0, // default: used as many times as needed required: true }, clientSecretTrustedIps: { @@ -118,6 +114,6 @@ const machineIdentitySchema = new Schema( } ); -machineIdentitySchema.index({ clientId: 1, isActive: 1 }) +machineIdentitySchema.index({ clientId: 1 }) export const MachineIdentity = model("MachineIdentity", machineIdentitySchema); \ No newline at end of file diff --git a/backend/src/models/machineIdentityClientSecret.ts b/backend/src/models/machineIdentityClientSecret.ts index 93b1851ef..6245efabe 100644 --- a/backend/src/models/machineIdentityClientSecret.ts +++ b/backend/src/models/machineIdentityClientSecret.ts @@ -3,17 +3,16 @@ import { Document, Schema, Types, model } from "mongoose"; export interface IMachineIdentityClientSecret extends Document { _id: Types.ObjectId; machineIdentity: Types.ObjectId; - isActive: boolean; description: string; clientSecretPrefix: string; clientSecretHash: string; - clientSecretLastUsed?: Date; + clientSecretLastUsedAt?: Date; clientSecretNumUses: number; clientSecretNumUsesLimit: number; clientSecretTTL: number; - accessTokenVersion: number; updatedAt: Date; createdAt: Date; + isClientSecretRevoked: boolean; } const machineIdentityClientSecretSchema = new Schema( @@ -23,11 +22,6 @@ const machineIdentityClientSecretSchema = new Schema( ref: "MachineIdentity", required: true }, - isActive: { - type: Boolean, - default: true, - required: true - }, description: { type: String, required: true @@ -40,7 +34,7 @@ const machineIdentityClientSecretSchema = new Schema( type: String, required: true }, - clientSecretLastUsed: { + clientSecretLastUsedAt: { type: Date, required: false }, @@ -63,11 +57,11 @@ const machineIdentityClientSecretSchema = new Schema( default: 0, // default: does not expire required: true }, - accessTokenVersion: { - type: Number, - default: 1, + isClientSecretRevoked: { + type: Boolean, + default: false, required: true - }, + } }, { timestamps: true @@ -75,7 +69,7 @@ const machineIdentityClientSecretSchema = new Schema( ); machineIdentityClientSecretSchema.index( - { machineIdentity: 1, isActive: 1 } + { machineIdentity: 1, isClientSecretRevoked: 1 } ) export const MachineIdentityClientSecret = model("MachineIdentityClientSecret", machineIdentityClientSecretSchema); \ No newline at end of file diff --git a/backend/src/utils/authn/authModeValidators/machineIdentity.ts b/backend/src/utils/authn/authModeValidators/machineIdentity.ts index 31e317d20..c99f4d3b1 100644 --- a/backend/src/utils/authn/authModeValidators/machineIdentity.ts +++ b/backend/src/utils/authn/authModeValidators/machineIdentity.ts @@ -1,6 +1,8 @@ import jwt from "jsonwebtoken"; -import { Types } from "mongoose"; -import { MachineIdentity, MachineIdentityClientSecret } from "../../../models"; +import { + IMachineIdentity, + IdentityAccessToken, +} from "../../../models"; import { getAuthSecret } from "../../../config"; import { AuthTokenType } from "../../../variables"; import { UnauthorizedRequestError } from "../../errors"; @@ -18,34 +20,80 @@ export const validateMachineIdentity = async ({ if (decodedToken.authTokenType !== AuthTokenType.MACHINE_ACCESS_TOKEN) throw UnauthorizedRequestError(); - const machineIdentityClientSecret = await MachineIdentityClientSecret.findOne({ - _id: new Types.ObjectId(decodedToken.clientSecretDataId), - isActive: true - }); + const machineIdentityAccessToken = await IdentityAccessToken + .findOne({ + _id: decodedToken.identityAccessTokenId, + isAccessTokenRevoked: false + }) + .populate<{ machineIdentity: IMachineIdentity }>("machineIdentity"); - if (!machineIdentityClientSecret) throw UnauthorizedRequestError(); + if (!machineIdentityAccessToken || !machineIdentityAccessToken?.machineIdentity) throw UnauthorizedRequestError(); - if (decodedToken.tokenVersion !== machineIdentityClientSecret.accessTokenVersion) { - // TODO: raise alarm + const { + accessTokenNumUsesLimit, + accessTokenNumUses, + accessTokenTTL, + accessTokenLastRenewedAt, + accessTokenMaxTTL, + createdAt: accessTokenCreatedAt + } = machineIdentityAccessToken; + + // ttl check + if (accessTokenTTL > 0) { + const currentDate = new Date(); + if (accessTokenLastRenewedAt) { + // access token has been renewed + const accessTokenRenewed = new Date(accessTokenLastRenewedAt); + const ttlInMilliseconds = accessTokenTTL * 1000; + const expirationTime = new Date(accessTokenRenewed.getTime() + ttlInMilliseconds); + + if (currentDate > expirationTime) throw UnauthorizedRequestError({ + message: "Failed to authenticate MI access token due to TTL expiration" + }); + } else { + // access token has never been renewed + const accessTokenCreated = new Date(accessTokenCreatedAt); + const ttlInMilliseconds = accessTokenTTL * 1000; + const expirationTime = new Date(accessTokenCreated.getTime() + ttlInMilliseconds); + + if (currentDate > expirationTime) throw UnauthorizedRequestError({ + message: "Failed to authenticate MI access token due to TTL expiration" + }); + } + } + + // max ttl check + if (accessTokenMaxTTL > 0) { + const accessTokenCreated = new Date(accessTokenCreatedAt); + const ttlInMilliseconds = accessTokenMaxTTL * 1000; + const currentDate = new Date(); + const expirationTime = new Date(accessTokenCreated.getTime() + ttlInMilliseconds); + + if (currentDate > expirationTime) throw UnauthorizedRequestError({ + message: "Failed to authenticate MI access token due to Max TTL expiration" + }); + } + + // num uses check + if ( + accessTokenNumUsesLimit > 0 + && accessTokenNumUses === accessTokenNumUsesLimit + ) { throw UnauthorizedRequestError({ - message: "Failed to authenticate", + message: "Failed to authenticate MI access token due to access token number of uses limit reached" }); } - const machineIdentity = await MachineIdentity.findByIdAndUpdate( - machineIdentityClientSecret.machineIdentity, + await IdentityAccessToken.findByIdAndUpdate( + machineIdentityAccessToken._id, { - accessTokenLastUsed: new Date(), - $inc: { accessTokenUsageCount: 1 } + accessTokenLastUsedAt: new Date(), + $inc: { accessTokenNumUses: 1 } }, { new: true } ); - - if (!machineIdentity) throw UnauthorizedRequestError({ - message: "Failed to authenticate" - }); - return machineIdentity; + return machineIdentityAccessToken.machineIdentity; } \ No newline at end of file diff --git a/backend/src/validation/machineIdentity.ts b/backend/src/validation/machineIdentity.ts index 74d3ee92a..cecff3b4a 100644 --- a/backend/src/validation/machineIdentity.ts +++ b/backend/src/validation/machineIdentity.ts @@ -1,13 +1,13 @@ import { z } from "zod"; import { NO_ACCESS } from "../variables"; -export const GetClientSecretsV3 = z.object({ +export const GetClientSecretsV1 = z.object({ params: z.object({ machineId: z.string() }) }); -export const CreateClientSecretV3 = z.object({ +export const CreateClientSecretV1 = z.object({ params: z.object({ machineId: z.string() }), @@ -18,21 +18,27 @@ export const CreateClientSecretV3 = z.object({ }), }); -export const DeleteClientSecretV3 = z.object({ +export const DeleteClientSecretV1 = z.object({ params: z.object({ machineId: z.string(), clientSecretId: z.string() }) }); -export const LoginMachineIdentityV3 = z.object({ +export const LoginMachineIdentityV1 = z.object({ body: z.object({ clientId: z.string().trim(), clientSecret: z.string().trim() }) }); -export const CreateMachineIdentityV3 = z.object({ +export const RenewAccessTokenV1 = z.object({ + body: z.object({ + accessToken: z.string().trim() + }) +}); + +export const CreateMachineIdentityV1 = z.object({ body: z.object({ name: z.string().trim(), organizationId: z.string().trim(), @@ -51,11 +57,17 @@ export const CreateMachineIdentityV3 = z.object({ .array() .min(1) .default([{ ipAddress: "0.0.0.0/0" }]), - accessTokenTTL: z.number().int().min(1).default(7200) + accessTokenTTL: z.number().int().min(0).default(7200), + accessTokenMaxTTL: z.number().int().min(0).default(7200), + accessTokenNumUsesLimit: z.number().int().min(0).default(0) + }) + .refine(data => data.accessTokenTTL <= data.accessTokenMaxTTL, { + message: "accessTokenTTL cannot be greater than accessTokenMaxTTL", + path: ["accessTokenTTL"], }) }); -export const UpdateMachineIdentityV3 = z.object({ +export const UpdateMachineIdentityV1 = z.object({ params: z.object({ machineId: z.string() }), @@ -76,11 +88,12 @@ export const UpdateMachineIdentityV3 = z.object({ .array() .min(1) .optional(), - accessTokenTTL: z.number().int().min(1).optional() + accessTokenTTL: z.number().int().min(0).optional(), + accessTokenNumUsesLimit: z.number().int().min(0).optional() }), }); -export const DeleteMachineIdentityV3 = z.object({ +export const DeleteMachineIdentityV1 = z.object({ params: z.object({ machineId: z.string() }), diff --git a/docs/documentation/platform/machine-identity.mdx b/docs/documentation/platform/machine-identity.mdx index e1255d527..57a82fb13 100644 --- a/docs/documentation/platform/machine-identity.mdx +++ b/docs/documentation/platform/machine-identity.mdx @@ -19,8 +19,10 @@ fetch secrets back from the `/` path of the `development` environment in some pr Here's a few pointers to get you acquainted with MIs: -- Each MI has a **Client ID** for which you can generate one or more **Client Secret(s)**. Together, a **Client ID** and **Client Secret** can be exchanged for an access token to authenticate with the Infisical API. -- MIs support IP allowlisting; this means you can restrict the usage of a MI access token to a specific IP or CIDR range. +- Each MI has a **Client ID** for which you can generate one or more **Client Secret(s)**. Together, a **Client ID** and **Client Secret** can be exchanged for an access token (i.e. login operation) to authenticate with the Infisical API. +- MIs support restrictions on the number of times that the **Client Secret(s)** and access token(s) can be used. +- MIs support token renewal that is the ability to extend the lifetime of a token by its TTL up to its maximum TTL since its creation. +- MIs support IP allowlisting; this means you can restrict the usage of **Client Secret(s)** and access token to a specific IP or CIDR range. - MIs rely on the role-based permission system to provision access to resources like secrets. - MIs support expiration, so, if specified, the client secret of the MI will automatically be defunct after a period of time. - MIs tracks most recent usage of their client secrets and access tokens; they also keep track of each token's usage count. @@ -42,7 +44,9 @@ In the following steps, we explore how to create and use MIs for your applicatio - Name (required): A friendly name for the MI - Role (required): A role from the **Organization Roles** tab to permit the MI to access certain resources. - - Access Token TTL: The time-to-live for each acccess token in seconds. + - Access Token Max TTL (default is `7200`): The maximum lifetime for an acccess token in seconds; a value of `0` implies an infinite maximum lifetime. + - Access Token TTL (default is `7200`): The incremental lifetime for an acccess token in seconds; a value of `0` implies an infinite incremental lifetime. + - Access Token Max Number of Uses (default is `0`): The maximum number of times that an access token can be used; a value of `0` implies infinite number of uses. - Client Secret Trusted IPs: The IPs or CIDR ranges that the **Client Secret** can be used from together with the **Client ID** to get back an access token. By default, **Client Secrets** are given the `0.0.0.0/0` entry representing all possible IPv4 addresses. - Access Token Trusted IPs: The IPs or CIDR ranges that access tokens can be used from. By default, each token is given the `0.0.0.0/0` entry representing all possible IPv4 addresses. @@ -66,7 +70,8 @@ In the following steps, we explore how to create and use MIs for your applicatio Feel free to input any (optional) details for the **Client Secret** configuration: - Description: A description for the **Client Secret**. - - TTL: The time-to-live for the **Client Secret**. By default, the TTL will be set to 0 which implies that the **Client Secret** will never expire. + - TTL (default is `0`): The time-to-live for the **Client Secret**. By default, the TTL will be set to 0 which implies that the **Client Secret** will never expire; a value of `0` implies an infinite lifetime. + - Max Number of Uses (default is `0`): The maximum number of times that the **Client Secret** can be used together with the **Client ID** to get back an access token; a value of `0` implies infinite number of uses. To enable the MI to access project-level resources such as secrets within a specific project, you should add it to that project. @@ -121,8 +126,8 @@ In the following steps, we explore how to create and use MIs for your applicatio A service token is a project-level authentication method that is being phased out in favor of MIs. - Amongst many differences, MIs provide broader access over the Infisical API with the same role-based - permission system used by users. + Amongst many differences, MIs provide broader access over the Infisical API, utilizes the same role-based + permission system used by users, and comes with ample more configurable security measures. There are a few reasons for why this might happen: @@ -132,6 +137,15 @@ In the following steps, we explore how to create and use MIs for your applicatio - You are attempting to access a `/raw` secrets endpoint that requires your project to disable E2EE. - The client secret/access token is being used from an untrusted IP. + + A MI access token can have a time-to-live (TTL) or incremental lifetime afterwhich it expires. + + In certain cases, you may want to extend the lifespan of an access token; to do so, you must use the max TTL parameter. + When TTL and max TTL are equal, a token is not renewable; when max TTL is greater than TTL, a token is renewable. + In the latter case, a token still expires at its TTL but its lifetime can be extended/renewed up until its max TLL. + + Note that the max TTL cannot be less than the TTL for an access token. + There are a few reasons for why this might happen: diff --git a/frontend/src/hooks/api/machineIdentities/mutations.tsx b/frontend/src/hooks/api/machineIdentities/mutations.tsx index 675d98704..ad60cbf78 100644 --- a/frontend/src/hooks/api/machineIdentities/mutations.tsx +++ b/frontend/src/hooks/api/machineIdentities/mutations.tsx @@ -34,14 +34,14 @@ export const useCreateMachineIdentityClientSecret = () => { machineId, description, ttl, - usageLimit + numUsesLimit }) => { const { data } = await apiRequest.post(`/api/v1/machine-identities/${machineId}/client-secrets`, { machineId, description, ttl, - usageLimit + numUsesLimit }); return data; @@ -80,7 +80,8 @@ export const useUpdateMachineIdentity = () => { role, clientSecretTrustedIps, accessTokenTrustedIps, - accessTokenTTL + accessTokenTTL, + accessTokenNumUsesLimit }) => { const { data: { machineIdentity } } = await apiRequest.patch(`/api/v1/machine-identities/${machineId}`, { @@ -89,6 +90,7 @@ export const useUpdateMachineIdentity = () => { clientSecretTrustedIps, accessTokenTrustedIps, accessTokenTTL, + accessTokenNumUsesLimit }); return machineIdentity; diff --git a/frontend/src/hooks/api/machineIdentities/types.ts b/frontend/src/hooks/api/machineIdentities/types.ts index a54975505..2c5579fd9 100644 --- a/frontend/src/hooks/api/machineIdentities/types.ts +++ b/frontend/src/hooks/api/machineIdentities/types.ts @@ -12,10 +12,9 @@ export type MachineIdentity = { clientId: string; name: string; organization: string; - isActive: boolean; accessTokenTTL: number; - accessTokenLastUsed?: string; - accessTokenUsageCount: number; + accessTokenMaxTTL: number; + accessTokenNumUsesLimit: number; clientSecretTrustedIps: MachineTrustedIp[]; accessTokenTrustedIps: MachineTrustedIp[]; createdAt: string; @@ -25,7 +24,6 @@ export type MachineIdentity = { export type MachineIdentityClientSecret = { _id: string; machineIdentity: string; - isActive: boolean; description: string; clientSecretPrefix: string; clientSecretNumUses: number; @@ -33,6 +31,7 @@ export type MachineIdentityClientSecret = { clientSecretTTL: number; createdAt: string; updatedAt: string; + isClientSecretRevoked: boolean; } export type MachineMembershipOrg = { @@ -66,13 +65,15 @@ export type CreateMachineIdentityDTO = { ipAddress: string; }[]; accessTokenTTL: number; + accessTokenMaxTTL: number; + accessTokenNumUsesLimit: number; } export type CreateMachineIdentityClientSecretDTO = { machineId: string; description?: string; ttl?: number; - usageLimit?: number; + numUsesLimit?: number; } export type CreateMachineIdentityClientSecretRes = { @@ -100,6 +101,8 @@ export type UpdateMachineIdentityDTO = { ipAddress: string; }[]; accessTokenTTL?: number; + accessTokenMaxTTL?: number; + accessTokenNumUsesLimit?: number; } export type DeleteMachineIdentityDTO = { diff --git a/frontend/src/views/Org/MembersPage/components/OrgMachineIdentityTab/components/MachineIdentitySection/AddMachineIdentityModal.tsx b/frontend/src/views/Org/MembersPage/components/OrgMachineIdentityTab/components/MachineIdentitySection/AddMachineIdentityModal.tsx index a6c026d3b..bb0c01728 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgMachineIdentityTab/components/MachineIdentitySection/AddMachineIdentityModal.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgMachineIdentityTab/components/MachineIdentitySection/AddMachineIdentityModal.tsx @@ -43,15 +43,13 @@ const schema = yup.object({ name: yup.string().required("MI name is required"), accessTokenTTL: yup .string() - .test("is-positive-integer", "Access Token TTL must be a positive integer", (value) => { - if (typeof value === "undefined") { - return false; - } - - const num = parseInt(value, 10); - return !Number.isNaN(num) && num > 0 && String(num) === value; - }) .required("Access Token TTL is required"), + accessTokenMaxTTL: yup + .string() + .required("Access Max Token TTL is required"), + accessTokenNumUsesLimit: yup + .string() + .required("Access Token Max Number of Uses is required"), role: yup.string(), clientSecretTrustedIps: yup .array( @@ -111,6 +109,8 @@ export const AddMachineIdentityModal = ({ defaultValues: { name: "", accessTokenTTL: "7200", + accessTokenMaxTTL: "7200", + accessTokenNumUsesLimit: "0", clientSecretTrustedIps: [{ ipAddress: "0.0.0.0/0" }], @@ -143,6 +143,8 @@ export const AddMachineIdentityModal = ({ clientSecretTrustedIps: MachineTrustedIp[]; accessTokenTrustedIps: MachineTrustedIp[]; accessTokenTTL: number; + accessTokenMaxTTL: number; + accessTokenNumUsesLimit: number; }; if (!roles?.length) return; @@ -150,6 +152,7 @@ export const AddMachineIdentityModal = ({ if (machineIdentity) { reset({ name: machineIdentity.name, + accessTokenNumUsesLimit: String(machineIdentity.accessTokenNumUsesLimit), role: machineIdentity?.customRole?.slug ?? machineIdentity.role, clientSecretTrustedIps: machineIdentity.clientSecretTrustedIps.map(({ ipAddress, @@ -167,12 +170,15 @@ export const AddMachineIdentityModal = ({ ipAddress: `${ipAddress}${prefix !== undefined ? `/${prefix}` : ""}` }); }), - accessTokenTTL: String(machineIdentity.accessTokenTTL) + accessTokenTTL: String(machineIdentity.accessTokenTTL), + accessTokenMaxTTL: String(machineIdentity.accessTokenMaxTTL) }); } else { reset({ name: "", accessTokenTTL: "7200", + accessTokenMaxTTL: "7200", + accessTokenNumUsesLimit: "0", role: roles[0].slug, clientSecretTrustedIps: [{ ipAddress: "0.0.0.0/0" @@ -198,9 +204,11 @@ export const AddMachineIdentityModal = ({ const onFormSubmit = async ({ name, accessTokenTTL, + accessTokenMaxTTL, role, clientSecretTrustedIps, - accessTokenTrustedIps + accessTokenTrustedIps, + accessTokenNumUsesLimit }: FormData) => { try { @@ -219,7 +227,9 @@ export const AddMachineIdentityModal = ({ role: role || undefined, clientSecretTrustedIps, accessTokenTrustedIps, - accessTokenTTL: Number(accessTokenTTL) + accessTokenTTL: Number(accessTokenTTL), + accessTokenMaxTTL: Number(accessTokenMaxTTL), + accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit) }); handlePopUpToggle("machineIdentity", false); @@ -232,6 +242,8 @@ export const AddMachineIdentityModal = ({ clientSecretTrustedIps, accessTokenTrustedIps, accessTokenTTL: Number(accessTokenTTL), + accessTokenMaxTTL: Number(accessTokenMaxTTL), + accessTokenNumUsesLimit: Number(accessTokenNumUsesLimit) }); handlePopUpToggle("machineIdentity", false); @@ -355,6 +367,26 @@ export const AddMachineIdentityModal = ({ )} /> */} + ( + + + + )} + /> + + )} + /> + ( + + )} diff --git a/frontend/src/views/Org/MembersPage/components/OrgMachineIdentityTab/components/MachineIdentitySection/CreateClientSecretModal.tsx b/frontend/src/views/Org/MembersPage/components/OrgMachineIdentityTab/components/MachineIdentitySection/CreateClientSecretModal.tsx index 090944069..9d7a57730 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgMachineIdentityTab/components/MachineIdentitySection/CreateClientSecretModal.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgMachineIdentityTab/components/MachineIdentitySection/CreateClientSecretModal.tsx @@ -35,7 +35,8 @@ import { UsePopUpState } from "@app/hooks/usePopUp"; const schema = yup.object({ description: yup.string(), - ttl: yup.string() // TODO: optional + ttl: yup.string(), + numUsesLimit: yup.string() }); export type FormData = yup.InferType; @@ -81,7 +82,8 @@ export const CreateClientSecretModal = ({ resolver: yupResolver(schema), defaultValues: { description: "", - ttl: "" + ttl: "", + numUsesLimit: "" } }); @@ -101,7 +103,8 @@ export const CreateClientSecretModal = ({ const onFormSubmit = async ({ description, - ttl + ttl, + numUsesLimit }: FormData) => { try { @@ -110,7 +113,8 @@ export const CreateClientSecretModal = ({ const { clientSecret } = await createClientSecretMutateAsync({ machineId: popUpData.machineId, description, - ttl: Number(ttl) + ttl: Number(ttl), + numUsesLimit: Number(numUsesLimit) }); setToken(clientSecret); @@ -211,7 +215,7 @@ export const CreateClientSecretModal = ({ ) : (
)} /> - ( - -
- - -
-
- )} - /> +
+ ( + +
+ + +
+
+ )} + /> + ( + +
+ + +
+
+ )} + /> +
)}

Client Secrets

@@ -270,13 +299,14 @@ export const CreateClientSecretModal = ({ Description + Num Uses Expires At Client Secret - {isLoading && } + {isLoading && } {!isLoading && data && data.length > 0 && @@ -284,19 +314,23 @@ export const CreateClientSecretModal = ({ _id, description, clientSecretTTL, - clientSecretPrefix + clientSecretPrefix, + clientSecretNumUses, + clientSecretNumUsesLimit, + createdAt }) => { let expiresAt; if (clientSecretTTL > 0) { - expiresAt = new Date(new Date().getTime() + clientSecretTTL * 1000); + expiresAt = new Date(new Date(createdAt).getTime() + clientSecretTTL * 1000); } return ( - + {description === "" ? "-" : description} + {`${clientSecretNumUses}${clientSecretNumUsesLimit ? `/${clientSecretNumUsesLimit}` : ""}`} {expiresAt ? format(expiresAt, "yyyy-MM-dd") : "-"} - {`${clientSecretPrefix}************`} - + {`${clientSecretPrefix}****`} + { handlePopUpOpen("deleteClientSecret", { @@ -308,7 +342,6 @@ export const CreateClientSecretModal = ({ colorSchema="primary" variant="plain" ariaLabel="update" - className="ml-4" > @@ -318,7 +351,7 @@ export const CreateClientSecretModal = ({ })} {!isLoading && data && data?.length === 0 && ( - + diff --git a/frontend/src/views/Org/MembersPage/components/OrgMachineIdentityTab/components/MachineIdentitySection/MachineIdentityTable.tsx b/frontend/src/views/Org/MembersPage/components/OrgMachineIdentityTab/components/MachineIdentitySection/MachineIdentityTable.tsx index 2e53c30c3..783c27a4d 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgMachineIdentityTab/components/MachineIdentitySection/MachineIdentityTable.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgMachineIdentityTab/components/MachineIdentitySection/MachineIdentityTable.tsx @@ -42,7 +42,9 @@ type Props = { }; clientSecretTrustedIps?: MachineTrustedIp[]; accessTokenTrustedIps?: MachineTrustedIp[]; + accessTokenMaxTTL?: number; accessTokenTTL?: number; + accessTokenNumUsesLimit?: number; } ) => void; }; @@ -148,7 +150,9 @@ export const MachineIdentityTable = ({ accessTokenTrustedIps, // createdAt, // expiresAt, + accessTokenMaxTTL, accessTokenTTL, + accessTokenNumUsesLimit }, role, customRole @@ -223,6 +227,8 @@ export const MachineIdentityTable = ({ clientSecretTrustedIps, accessTokenTrustedIps, accessTokenTTL, + accessTokenMaxTTL, + accessTokenNumUsesLimit }); }} size="lg"