diff --git a/backend/src/services/identity-access-token/identity-access-token-dal.ts b/backend/src/services/identity-access-token/identity-access-token-dal.ts index f12bd8c15..57517c706 100644 --- a/backend/src/services/identity-access-token/identity-access-token-dal.ts +++ b/backend/src/services/identity-access-token/identity-access-token-dal.ts @@ -37,7 +37,7 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => { ) .leftJoin(TableName.IdentityOidcAuth, `${TableName.Identity}.id`, `${TableName.IdentityOidcAuth}.identityId`) .leftJoin(TableName.IdentityTokenAuth, `${TableName.Identity}.id`, `${TableName.IdentityTokenAuth}.identityId`) - + .leftJoin(TableName.IdentityJwtAuth, `${TableName.Identity}.id`, `${TableName.IdentityJwtAuth}.identityId`) .select(selectAllTableCols(TableName.IdentityAccessToken)) .select( db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityUniversalAuth).as("accessTokenTrustedIpsUa"), @@ -47,6 +47,7 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => { db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityKubernetesAuth).as("accessTokenTrustedIpsK8s"), db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityOidcAuth).as("accessTokenTrustedIpsOidc"), db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityTokenAuth).as("accessTokenTrustedIpsToken"), + db.ref("accessTokenTrustedIps").withSchema(TableName.IdentityJwtAuth).as("accessTokenTrustedIpsJwt"), db.ref("name").withSchema(TableName.Identity) ) .first(); @@ -61,7 +62,8 @@ export const identityAccessTokenDALFactory = (db: TDbClient) => { trustedIpsAzureAuth: doc.accessTokenTrustedIpsAzure, trustedIpsKubernetesAuth: doc.accessTokenTrustedIpsK8s, trustedIpsOidcAuth: doc.accessTokenTrustedIpsOidc, - trustedIpsAccessTokenAuth: doc.accessTokenTrustedIpsToken + trustedIpsAccessTokenAuth: doc.accessTokenTrustedIpsToken, + trustedIpsAccessJwtAuth: doc.accessTokenTrustedIpsJwt }; } catch (error) { throw new DatabaseError({ error, name: "IdAccessTokenFindOne" }); diff --git a/backend/src/services/identity-access-token/identity-access-token-service.ts b/backend/src/services/identity-access-token/identity-access-token-service.ts index a59d1e959..47d1791d2 100644 --- a/backend/src/services/identity-access-token/identity-access-token-service.ts +++ b/backend/src/services/identity-access-token/identity-access-token-service.ts @@ -171,7 +171,8 @@ export const identityAccessTokenServiceFactory = ({ [IdentityAuthMethod.AZURE_AUTH]: identityAccessToken.trustedIpsAzureAuth, [IdentityAuthMethod.KUBERNETES_AUTH]: identityAccessToken.trustedIpsKubernetesAuth, [IdentityAuthMethod.OIDC_AUTH]: identityAccessToken.trustedIpsOidcAuth, - [IdentityAuthMethod.TOKEN_AUTH]: identityAccessToken.trustedIpsAccessTokenAuth + [IdentityAuthMethod.TOKEN_AUTH]: identityAccessToken.trustedIpsAccessTokenAuth, + [IdentityAuthMethod.JWT_AUTH]: identityAccessToken.trustedIpsAccessJwtAuth }; const trustedIps = trustedIpsMap[identityAccessToken.authMethod as IdentityAuthMethod]; diff --git a/backend/src/services/identity-jwt-auth/identity-jwt-auth-service.ts b/backend/src/services/identity-jwt-auth/identity-jwt-auth-service.ts index f0618b715..73cb0090d 100644 --- a/backend/src/services/identity-jwt-auth/identity-jwt-auth-service.ts +++ b/backend/src/services/identity-jwt-auth/identity-jwt-auth-service.ts @@ -127,29 +127,47 @@ export const identityJwtAuthServiceFactory = ({ } if (identityJwtAuth.boundIssuer) { + if (!tokenData.iss) { + throw new UnauthorizedError({ + message: "Access denied: token has no issuer field" + }); + } + if (!doesFieldValueMatchJwtPolicy(tokenData.iss, identityJwtAuth.boundIssuer)) { throw new ForbiddenRequestError({ - message: "Access denied: issuer mismatch." + message: "Access denied: issuer mismatch" }); } } if (identityJwtAuth.boundSubject) { + if (!tokenData.sub) { + throw new UnauthorizedError({ + message: "Access denied: token has no subject field" + }); + } + if (!doesFieldValueMatchJwtPolicy(tokenData.sub, identityJwtAuth.boundSubject)) { throw new ForbiddenRequestError({ - message: "Access denied: subject not allowed." + message: "Access denied: subject not allowed" }); } } if (identityJwtAuth.boundAudiences) { + if (!tokenData.aud) { + throw new UnauthorizedError({ + message: "Access denied: token has no audience field" + }); + } + if ( !identityJwtAuth.boundAudiences .split(", ") .some((policyValue) => doesFieldValueMatchJwtPolicy(tokenData.aud, policyValue)) ) { throw new UnauthorizedError({ - message: "Access denied: audience not allowed." + message: "Access denied: token audience not allowed" }); } } @@ -157,12 +175,19 @@ export const identityJwtAuthServiceFactory = ({ if (identityJwtAuth.boundClaims) { Object.keys(identityJwtAuth.boundClaims).forEach((claimKey) => { const claimValue = (identityJwtAuth.boundClaims as Record)[claimKey]; + + if (!tokenData[claimKey]) { + throw new UnauthorizedError({ + message: `Access denied: token has no ${claimKey} field` + }); + } + // handle both single and multi-valued claims if ( !claimValue.split(", ").some((claimEntry) => doesFieldValueMatchJwtPolicy(tokenData[claimKey], claimEntry)) ) { throw new UnauthorizedError({ - message: "Access denied: claim mismatch." + message: `Access denied: claim mismatch for field ${claimKey}` }); } }); @@ -389,7 +414,7 @@ export const identityJwtAuthServiceFactory = ({ orgId: actorOrgId }); - if (jwksCaCert) { + if (jwksCaCert !== undefined) { const { cipherTextBlob: encryptedJwksCaCert } = orgDataKeyEncryptor({ plainText: Buffer.from(jwksCaCert) }); diff --git a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityJwtAuthForm.tsx b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityJwtAuthForm.tsx index 5785f23e6..d31bd43bd 100644 --- a/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityJwtAuthForm.tsx +++ b/frontend/src/views/Org/MembersPage/components/OrgIdentityTab/components/IdentitySection/IdentityJwtAuthForm.tsx @@ -359,13 +359,31 @@ export const IdentityJwtAuthForm = ({ label={`Public Key ${index + 1}`} errorText={error?.message} isError={Boolean(error)} + icon={ + This field only accepts PEM-formatted public keys} + > + + + } >