diff --git a/packages/backend-for-frontend/src/services/toolService.ts b/packages/backend-for-frontend/src/services/toolService.ts index d286488c7a..5dc6f7b5f1 100644 --- a/packages/backend-for-frontend/src/services/toolService.ts +++ b/packages/backend-for-frontend/src/services/toolService.ts @@ -2,10 +2,13 @@ import { isAxiosError } from "axios"; import { + AgreementComponentState, ApiKey, ClientAssertion, ConsumerKey, + EServiceComponentState, FailedValidation, + PurposeComponentState, SuccessfulValidation, validateClientKindAndPlatformState, validateRequestParameters, @@ -16,9 +19,11 @@ import { AgreementId, ApiError, ClientId, + DescriptorId, EServiceId, ItemState, PurposeId, + PurposeVersionId, TenantId, unsafeBrandId, } from "pagopa-interop-models"; @@ -320,9 +325,9 @@ async function retrieveKeyAndEservice( consumerId: unsafeBrandId(keyWithClient.client.consumerId), agreementId: unsafeBrandId(agreement.id), eServiceId: unsafeBrandId(agreement.eserviceId), - agreementState: agreementStateToItemState(agreement.state), - purposeState: retrievePurposeItemState(purpose), - descriptorState: descriptorStateToItemState(descriptor.state), + agreementState: agreementStateToComponentState(agreement.state), + purposeState: purposeToComponentState(purpose), + eServiceState: descriptorToComponentState(descriptor), }, eservice, descriptor, @@ -374,7 +379,9 @@ async function retrieveDescriptor( return descriptor; } -function retrievePurposeItemState(purpose: purposeApi.Purpose): ItemState { +function purposeToComponentState( + purpose: purposeApi.Purpose +): PurposeComponentState { const purposeVersion = [...purpose.versions] .sort( (a, b) => @@ -391,9 +398,13 @@ function retrievePurposeItemState(purpose: purposeApi.Purpose): ItemState { throw missingActivePurposeVersion(purpose.id); } - return purposeVersion.state === purposeApi.PurposeVersionState.Enum.ACTIVE - ? ItemState.Enum.ACTIVE - : ItemState.Enum.INACTIVE; + return { + state: + purposeVersion.state === purposeApi.PurposeVersionState.Enum.ACTIVE + ? ItemState.Enum.ACTIVE + : ItemState.Enum.INACTIVE, + versionId: unsafeBrandId(purposeVersion.id), + }; } function toTokenValidationEService( @@ -408,20 +419,27 @@ function toTokenValidationEService( }; } -const agreementStateToItemState = ( +const agreementStateToComponentState = ( state: agreementApi.AgreementState -): ItemState => - state === agreementApi.AgreementState.Values.ACTIVE - ? ItemState.Enum.ACTIVE - : ItemState.Enum.INACTIVE; - -const descriptorStateToItemState = ( - state: catalogApi.EServiceDescriptorState -): ItemState => - state === catalogApi.EServiceDescriptorState.Enum.PUBLISHED || - state === catalogApi.EServiceDescriptorState.Enum.DEPRECATED - ? ItemState.Enum.ACTIVE - : ItemState.Enum.INACTIVE; +): AgreementComponentState => ({ + state: + state === agreementApi.AgreementState.Values.ACTIVE + ? ItemState.Enum.ACTIVE + : ItemState.Enum.INACTIVE, +}); + +const descriptorToComponentState = ( + descriptor: catalogApi.EServiceDescriptor +): EServiceComponentState => ({ + state: + descriptor.state === catalogApi.EServiceDescriptorState.Enum.PUBLISHED || + descriptor.state === catalogApi.EServiceDescriptorState.Enum.DEPRECATED + ? ItemState.Enum.ACTIVE + : ItemState.Enum.INACTIVE, + descriptorId: unsafeBrandId(descriptor.id), + audience: descriptor.audience, + voucherLifespan: descriptor.voucherLifespan, +}); function apiErrorsToValidationFailures( errors: Array> | undefined diff --git a/packages/client-assertion-validation/src/types.ts b/packages/client-assertion-validation/src/types.ts index 38a8729a83..7c55586126 100644 --- a/packages/client-assertion-validation/src/types.ts +++ b/packages/client-assertion-validation/src/types.ts @@ -3,9 +3,11 @@ import { ApiError, ClientId, clientKindTokenStates, + DescriptorId, EServiceId, ItemState, PurposeId, + PurposeVersionId, TenantId, } from "pagopa-interop-models"; import { z } from "zod"; @@ -50,7 +52,31 @@ export const ClientAssertion = z .strict(); export type ClientAssertion = z.infer; +const ComponentState = z.object({ + state: ItemState, +}); + +export type ComponentState = z.infer; + +const AgreementComponentState = ComponentState; +export type AgreementComponentState = z.infer; + +const EServiceComponentState = ComponentState.extend({ + descriptorId: DescriptorId, + audience: z.array(z.string()), + voucherLifespan: z.number(), +}); + +export type EServiceComponentState = z.infer; + +const PurposeComponentState = ComponentState.extend({ + versionId: PurposeVersionId, +}); + +export type PurposeComponentState = z.infer; + export const Base64Encoded = z.string().base64().min(1); + export const Key = z .object({ clientId: ClientId, @@ -65,11 +91,12 @@ export type Key = z.infer; export const ConsumerKey = Key.extend({ clientKind: z.literal(clientKindTokenStates.consumer), purposeId: PurposeId, - purposeState: ItemState, + // TODO: can we rename the type to purposeDetails or something similar (avoid misleading "state" term)? + purposeState: PurposeComponentState, agreementId: AgreementId, - agreementState: ItemState, + agreementState: AgreementComponentState, eServiceId: EServiceId, - descriptorState: ItemState, + eServiceState: EServiceComponentState, }).strict(); export type ConsumerKey = z.infer; diff --git a/packages/client-assertion-validation/src/utils.ts b/packages/client-assertion-validation/src/utils.ts index 6bfa958313..71654efea8 100644 --- a/packages/client-assertion-validation/src/utils.ts +++ b/packages/client-assertion-validation/src/utils.ts @@ -193,13 +193,17 @@ export const validatePlatformState = ( key: ConsumerKey ): ValidationResult => { const agreementError = - key.agreementState !== itemState.active ? inactiveAgreement() : undefined; + key.agreementState.state !== itemState.active + ? inactiveAgreement() + : undefined; const descriptorError = - key.descriptorState !== itemState.active ? inactiveEService() : undefined; + key.eServiceState.state !== itemState.active + ? inactiveEService() + : undefined; const purposeError = - key.purposeState !== itemState.active ? inactivePurpose() : undefined; + key.purposeState.state !== itemState.active ? inactivePurpose() : undefined; if (!agreementError && !descriptorError && !purposeError) { return successfulValidation(key); diff --git a/packages/client-assertion-validation/test/utils.ts b/packages/client-assertion-validation/test/utils.ts index 2ad7728e8f..38e6a207d6 100644 --- a/packages/client-assertion-validation/test/utils.ts +++ b/packages/client-assertion-validation/test/utils.ts @@ -2,12 +2,17 @@ import crypto from "crypto"; import { ClientId, clientKindTokenStates, + DescriptorId, generateId, itemState, PurposeId, + PurposeVersionId, TenantId, } from "pagopa-interop-models"; -import * as jose from "jose"; +import { + generateKeySet, + getMockClientAssertion, +} from "pagopa-interop-commons-test"; import { ApiKey, ClientAssertionValidationRequest, @@ -21,96 +26,7 @@ import { export const value64chars = crypto.randomBytes(32).toString("hex"); -export const getMockClientAssertion = async (props?: { - standardClaimsOverride?: Partial; - customClaims?: { [k: string]: unknown }; - customHeader?: { [k: string]: unknown }; -}): Promise<{ - jws: string; - publicKeyEncodedPem: string; -}> => { - const { keySet, publicKeyEncodedPem } = generateKeySet(); - - const clientId = generateId(); - const defaultPayload: jose.JWTPayload = { - iss: clientId, - sub: clientId, - aud: ["test.interop.pagopa.it", "dev.interop.pagopa.it"], - exp: 60, - jti: generateId(), - iat: 5, - }; - - const actualPayload: jose.JWTPayload = { - ...defaultPayload, - ...props?.standardClaimsOverride, - ...props?.customClaims, - }; - - const headers: jose.JWTHeaderParameters = { - alg: "RS256", - kid: "kid", - ...props?.customHeader, - }; - - const jws = await signClientAssertion({ - payload: actualPayload, - headers, - keySet, - }); - - return { - jws, - publicKeyEncodedPem, - }; -}; - -export const generateKeySet = (): { - keySet: crypto.KeyPairKeyObjectResult; - publicKeyEncodedPem: string; -} => { - const keySet: crypto.KeyPairKeyObjectResult = crypto.generateKeyPairSync( - "rsa", - { - modulusLength: 2048, - } - ); - - const pemPublicKey = keySet.publicKey - .export({ - type: "spki", - format: "pem", - }) - .toString(); - - const publicKeyEncodedPem = Buffer.from(pemPublicKey).toString("base64"); - return { - keySet, - publicKeyEncodedPem, - }; -}; - -const signClientAssertion = async ({ - payload, - headers, - keySet, -}: { - payload: jose.JWTPayload; - headers: jose.JWTHeaderParameters; - keySet: crypto.KeyPairKeyObjectResult; -}): Promise => { - const pemPrivateKey = keySet.privateKey.export({ - type: "pkcs8", - format: "pem", - }); - - const privateKey = crypto.createPrivateKey(pemPrivateKey); - return await new jose.SignJWT(payload) - .setProtectedHeader(headers) - .sign(privateKey); -}; - -export const getMockKey = (): Key => ({ +export const getMockTokenKey = (): Key => ({ clientId: generateId(), consumerId: generateId(), kid: "kid", @@ -119,18 +35,26 @@ export const getMockKey = (): Key => ({ }); export const getMockConsumerKey = (): ConsumerKey => ({ - ...getMockKey(), + ...getMockTokenKey(), purposeId: generateId(), clientKind: clientKindTokenStates.consumer, - purposeState: itemState.active, + purposeState: { + state: itemState.active, + versionId: generateId(), + }, agreementId: generateId(), - agreementState: itemState.active, + agreementState: { state: itemState.active }, eServiceId: generateId(), - descriptorState: itemState.active, + eServiceState: { + state: itemState.active, + descriptorId: generateId(), + audience: ["test.interop.pagopa.it"], + voucherLifespan: 60, + }, }); export const getMockApiKey = (): ApiKey => ({ - ...getMockKey(), + ...getMockTokenKey(), clientKind: clientKindTokenStates.api, }); diff --git a/packages/client-assertion-validation/test/validation.test.ts b/packages/client-assertion-validation/test/validation.test.ts index d698ffe1d0..772aeec6cf 100644 --- a/packages/client-assertion-validation/test/validation.test.ts +++ b/packages/client-assertion-validation/test/validation.test.ts @@ -3,11 +3,17 @@ import { fail } from "assert"; import { describe, expect, it } from "vitest"; import { ClientId, + DescriptorId, generateId, itemState, PurposeId, + PurposeVersionId, } from "pagopa-interop-models"; import * as jsonwebtoken from "jsonwebtoken"; +import { + generateKeySet, + getMockClientAssertion, +} from "pagopa-interop-commons-test"; import { validateClientKindAndPlatformState, validateRequestParameters, @@ -53,12 +59,10 @@ import { Key, } from "../src/types.js"; import { - generateKeySet, getMockAccessTokenRequest, getMockApiKey, - getMockClientAssertion, getMockConsumerKey, - getMockKey, + getMockTokenKey, value64chars, } from "./utils.js"; @@ -493,7 +497,7 @@ describe("validation test", async () => { }, }); const mockKey = { - ...getMockKey(), + ...getMockTokenKey(), publicKey: publicKeyEncodedPem, }; const { errors } = await verifyClientAssertionSignature(jws, mockKey); @@ -504,7 +508,7 @@ describe("validation test", async () => { const { jws, publicKeyEncodedPem } = await getMockClientAssertion(); const mockKey = { - ...getMockKey(), + ...getMockTokenKey(), publicKey: Buffer.from(publicKeyEncodedPem, "base64").toString("utf8"), }; const { errors } = await verifyClientAssertionSignature(jws, mockKey); @@ -532,7 +536,7 @@ describe("validation test", async () => { }, }); const mockKey: Key = { - ...getMockKey(), + ...getMockTokenKey(), publicKey: publicKeyEncodedPem, algorithm: notAllowedAlg, }; @@ -557,7 +561,7 @@ describe("validation test", async () => { }); const mockKey = { - ...getMockKey(), + ...getMockTokenKey(), publicKey: publicKeyEncodedPem, }; const { errors } = await verifyClientAssertionSignature(jws, mockKey); @@ -568,7 +572,7 @@ describe("validation test", async () => { it("jsonWebTokenError", async () => { const { publicKeyEncodedPem } = generateKeySet(); const mockKey = { - ...getMockKey(), + ...getMockTokenKey(), publicKey: publicKeyEncodedPem, }; const { errors } = await verifyClientAssertionSignature( @@ -583,7 +587,7 @@ describe("validation test", async () => { it("invalidSignature", async () => { const { publicKeyEncodedPem } = generateKeySet(); const mockKey = { - ...getMockKey(), + ...getMockTokenKey(), publicKey: publicKeyEncodedPem, }; const { jws } = await getMockClientAssertion(); @@ -601,7 +605,7 @@ describe("validation test", async () => { it("jsonWebTokenError - malformed jwt", async () => { const { publicKeyEncodedPem } = generateKeySet(); const mockKey = { - ...getMockKey(), + ...getMockTokenKey(), publicKey: publicKeyEncodedPem, }; const { errors } = await verifyClientAssertionSignature( @@ -618,7 +622,7 @@ describe("validation test", async () => { await getMockClientAssertion(); const mockKey = { - ...getMockKey(), + ...getMockTokenKey(), publicKey: publicKeyEncodedPem, }; @@ -654,7 +658,7 @@ describe("validation test", async () => { }, }); const mockKey = { - ...getMockKey(), + ...getMockTokenKey(), publicKey: publicKeyEncodedPem, }; @@ -673,9 +677,17 @@ describe("validation test", async () => { it("success", async () => { const mockKey: ConsumerKey = { ...getMockConsumerKey(), - agreementState: itemState.active, - descriptorState: itemState.active, - purposeState: itemState.active, + agreementState: { state: itemState.active }, + eServiceState: { + state: itemState.active, + descriptorId: generateId(), + audience: ["test.interop.pagopa.it"], + voucherLifespan: 60, + }, + purposeState: { + state: itemState.active, + versionId: generateId(), + }, }; validatePlatformState(mockKey); const { errors } = validatePlatformState(mockKey); @@ -685,7 +697,7 @@ describe("validation test", async () => { it("inactiveAgreement", async () => { const mockKey: ConsumerKey = { ...getMockConsumerKey(), - agreementState: itemState.inactive, + agreementState: { state: itemState.inactive }, }; validatePlatformState(mockKey); const { errors } = validatePlatformState(mockKey); @@ -697,7 +709,12 @@ describe("validation test", async () => { it("inactiveEservice", async () => { const mockKey: ConsumerKey = { ...getMockConsumerKey(), - descriptorState: itemState.inactive, + eServiceState: { + state: itemState.inactive, + descriptorId: generateId(), + audience: ["test.interop.pagopa.it"], + voucherLifespan: 60, + }, }; validatePlatformState(mockKey); const { errors } = validatePlatformState(mockKey); @@ -709,7 +726,10 @@ describe("validation test", async () => { it("inactivePurpose", async () => { const mockKey: ConsumerKey = { ...getMockConsumerKey(), - purposeState: itemState.inactive, + purposeState: { + state: itemState.inactive, + versionId: generateId(), + }, }; validatePlatformState(mockKey); const { errors } = validatePlatformState(mockKey); @@ -721,9 +741,17 @@ describe("validation test", async () => { it("inactiveAgreement and inactiveEservice and inactivePurpose", async () => { const mockKey: ConsumerKey = { ...getMockConsumerKey(), - agreementState: itemState.inactive, - descriptorState: itemState.inactive, - purposeState: itemState.inactive, + agreementState: { state: itemState.inactive }, + eServiceState: { + state: itemState.inactive, + descriptorId: generateId(), + audience: ["test.interop.pagopa.it"], + voucherLifespan: 60, + }, + purposeState: { + state: itemState.inactive, + versionId: generateId(), + }, }; validatePlatformState(mockKey); const { errors } = validatePlatformState(mockKey); @@ -762,7 +790,12 @@ describe("validation test", async () => { it("inactiveEService (consumerKey with consumer client kind; invalid platform states)", async () => { const mockConsumerKey: ConsumerKey = { ...getMockConsumerKey(), - descriptorState: itemState.inactive, + eServiceState: { + state: itemState.inactive, + descriptorId: generateId(), + audience: ["test.interop.pagopa.it"], + voucherLifespan: 60, + }, }; const { data: mockClientAssertion } = verifyClientAssertion( ( @@ -825,7 +858,7 @@ describe("validation test", async () => { it("purposeIdNotProvided and platformStateError", async () => { const mockConsumerKey: ConsumerKey = { ...getMockConsumerKey(), - agreementState: itemState.inactive, + agreementState: { state: itemState.inactive }, }; const { data: mockClientAssertion } = verifyClientAssertion( ( diff --git a/packages/commons-test/package.json b/packages/commons-test/package.json index dde6a915e0..f639d59905 100644 --- a/packages/commons-test/package.json +++ b/packages/commons-test/package.json @@ -31,6 +31,7 @@ "aws-sdk-client-mock": "4.0.1", "axios": "1.7.4", "dotenv-flow": "4.1.0", + "jose": "5.9.4", "jsonwebtoken": "9.0.2", "pagopa-interop-commons": "workspace:*", "pagopa-interop-models": "workspace:*", diff --git a/packages/commons-test/src/testUtils.ts b/packages/commons-test/src/testUtils.ts index ff2f045b04..63d7a73882 100644 --- a/packages/commons-test/src/testUtils.ts +++ b/packages/commons-test/src/testUtils.ts @@ -1,3 +1,4 @@ +import crypto from "crypto"; import { generateMock } from "@anatine/zod-mock"; import { Agreement, @@ -58,9 +59,11 @@ import { PlatformStatesClientPK, PlatformStatesClientEntry, makePlatformStatesClientPK, + unsafeBrandId, } from "pagopa-interop-models"; import { AuthData } from "pagopa-interop-commons"; import { z } from "zod"; +import * as jose from "jose"; export function expectPastTimestamp(timestamp: bigint): boolean { return ( @@ -334,7 +337,9 @@ export const getMockAuthData = (organizationId?: TenantId): AuthData => ({ export const getMockTokenStatesClientPurposeEntry = ( tokenStateEntryPK?: TokenGenerationStatesClientKidPurposePK ): TokenGenerationStatesClientPurposeEntry => { - const clientId = generateId(); + const clientId = tokenStateEntryPK + ? unsafeBrandId(tokenStateEntryPK.split("#")[1]) + : generateId(); const purposeId = generateId(); const consumerId = generateId(); const eserviceId = generateId(); @@ -351,7 +356,7 @@ export const getMockTokenStatesClientPurposeEntry = ( kid, purposeId, }), - descriptorState: itemState.inactive, + descriptorState: itemState.active, descriptorAudience: ["pagopa.it/test1", "pagopa.it/test2"], descriptorVoucherLifespan: 60, updatedAt: new Date().toISOString(), @@ -372,7 +377,7 @@ export const getMockTokenStatesClientPurposeEntry = ( descriptorId, }), GSIPK_purposeId: purposeId, - purposeState: itemState.inactive, + purposeState: itemState.active, GSIPK_clientId_purposeId: makeGSIPKClientIdPurposeId({ clientId, purposeId, @@ -401,7 +406,10 @@ export const getMockAgreementEntry = ( export const getMockTokenStatesClientEntry = ( tokenStateEntryPK?: TokenGenerationStatesClientKidPK ): TokenGenerationStatesClientEntry => { - const clientId = generateId(); + const clientId = tokenStateEntryPK + ? unsafeBrandId(tokenStateEntryPK.split("#")[1]) + : generateId(); + const consumerId = generateId(); const kid = `kid ${Math.random()}`; @@ -432,3 +440,92 @@ export const getMockPlatformStatesClientEntry = ( clientConsumerId: generateId(), clientPurposesIds: [], }); + +export const getMockClientAssertion = async (props?: { + standardClaimsOverride?: Partial; + customClaims?: { [k: string]: unknown }; + customHeader?: { [k: string]: unknown }; +}): Promise<{ + jws: string; + publicKeyEncodedPem: string; +}> => { + const { keySet, publicKeyEncodedPem } = generateKeySet(); + + const clientId = generateId(); + const defaultPayload: jose.JWTPayload = { + iss: clientId, + sub: clientId, + aud: ["test.interop.pagopa.it", "dev.interop.pagopa.it"], + exp: 60, + jti: generateId(), + iat: 5, + }; + + const actualPayload: jose.JWTPayload = { + ...defaultPayload, + ...props?.standardClaimsOverride, + ...props?.customClaims, + }; + + const headers: jose.JWTHeaderParameters = { + alg: "RS256", + kid: "kid", + ...props?.customHeader, + }; + + const jws = await signClientAssertion({ + payload: actualPayload, + headers, + keySet, + }); + + return { + jws, + publicKeyEncodedPem, + }; +}; + +export const generateKeySet = (): { + keySet: crypto.KeyPairKeyObjectResult; + publicKeyEncodedPem: string; +} => { + const keySet: crypto.KeyPairKeyObjectResult = crypto.generateKeyPairSync( + "rsa", + { + modulusLength: 2048, + } + ); + + const pemPublicKey = keySet.publicKey + .export({ + type: "spki", + format: "pem", + }) + .toString(); + + const publicKeyEncodedPem = Buffer.from(pemPublicKey).toString("base64"); + return { + keySet, + publicKeyEncodedPem, + }; +}; + +const signClientAssertion = async ({ + payload, + headers, + keySet, +}: { + payload: jose.JWTPayload; + headers: jose.JWTHeaderParameters; + keySet: crypto.KeyPairKeyObjectResult; +}): Promise => { + const pemPrivateKey = keySet.privateKey.export({ + type: "pkcs8", + format: "pem", + }); + + const privateKey = crypto.createPrivateKey(pemPrivateKey); + return await new jose.SignJWT(payload) + .setProtectedHeader(headers) + .sign(privateKey); +}; diff --git a/packages/commons-test/src/tokenGenerationReadmodelUtils.ts b/packages/commons-test/src/tokenGenerationReadmodelUtils.ts index 18f5e7df98..ca231469ad 100644 --- a/packages/commons-test/src/tokenGenerationReadmodelUtils.ts +++ b/packages/commons-test/src/tokenGenerationReadmodelUtils.ts @@ -20,10 +20,46 @@ import { PlatformStatesPurposeEntry, PlatformStatesAgreementEntry, TokenGenerationStatesGenericEntry, + TokenGenerationStatesClientEntry, } from "pagopa-interop-models"; import { unmarshall } from "@aws-sdk/util-dynamodb"; import { z } from "zod"; +export const writeTokenStateClientEntry = async ( + tokenStateEntry: TokenGenerationStatesClientEntry, + dynamoDBClient: DynamoDBClient +): Promise => { + const input: PutItemInput = { + ConditionExpression: "attribute_not_exists(PK)", + Item: { + PK: { + S: tokenStateEntry.PK, + }, + updatedAt: { + S: tokenStateEntry.updatedAt, + }, + consumerId: { + S: tokenStateEntry.consumerId, + }, + clientKind: { + S: tokenStateEntry.clientKind, + }, + publicKey: { + S: tokenStateEntry.publicKey, + }, + GSIPK_clientId: { + S: tokenStateEntry.GSIPK_clientId, + }, + GSIPK_kid: { + S: tokenStateEntry.GSIPK_kid, + }, + }, + TableName: "token-generation-states", + }; + const command = new PutItemCommand(input); + await dynamoDBClient.send(command); +}; + export const writeTokenStateEntry = async ( tokenStateEntry: TokenGenerationStatesClientPurposeEntry, dynamoDBClient: DynamoDBClient diff --git a/packages/commons/src/config/authorizationServerTokenGenerationConfig.ts b/packages/commons/src/config/authorizationServerTokenGenerationConfig.ts new file mode 100644 index 0000000000..41fbec09ef --- /dev/null +++ b/packages/commons/src/config/authorizationServerTokenGenerationConfig.ts @@ -0,0 +1,24 @@ +import { z } from "zod"; + +export const AuthorizationServerTokenGenerationConfig = z + .object({ + GENERATED_INTEROP_TOKEN_ALGORITHM: z.string(), + GENERATED_INTEROP_TOKEN_KID: z.string(), + GENERATED_INTEROP_TOKEN_ISSUER: z.string(), + GENERATED_INTEROP_TOKEN_M2M_AUDIENCE: z.string(), + GENERATED_INTEROP_TOKEN_M2M_DURATION_SECONDS: z.string(), + }) + .transform((c) => ({ + generatedInteropTokenAlgorithm: c.GENERATED_INTEROP_TOKEN_ALGORITHM, + generatedInteropTokenKid: c.GENERATED_INTEROP_TOKEN_KID, + generatedInteropTokenIssuer: c.GENERATED_INTEROP_TOKEN_ISSUER, + generatedInteropTokenM2MAudience: c.GENERATED_INTEROP_TOKEN_M2M_AUDIENCE, + generatedInteropTokenM2MDurationSeconds: parseInt( + c.GENERATED_INTEROP_TOKEN_M2M_DURATION_SECONDS, + 10 + ), + })); + +export type AuthorizationServerTokenGenerationConfig = z.infer< + typeof AuthorizationServerTokenGenerationConfig +>; diff --git a/packages/commons/src/config/index.ts b/packages/commons/src/config/index.ts index 7cba6068cc..6993d3b5d9 100644 --- a/packages/commons/src/config/index.ts +++ b/packages/commons/src/config/index.ts @@ -15,3 +15,4 @@ export * from "./sessionTokenGenerationConfig.js"; export * from "./redisRateLimiterConfig.js"; export * from "./pecEmailManagerConfig.js"; export * from "./selfcareConfig.js"; +export * from "./authorizationServerTokenGenerationConfig.js"; diff --git a/packages/commons/src/interop-token/interopTokenService.ts b/packages/commons/src/interop-token/interopTokenService.ts index b8088b4767..c59cb52cba 100644 --- a/packages/commons/src/interop-token/interopTokenService.ts +++ b/packages/commons/src/interop-token/interopTokenService.ts @@ -1,12 +1,26 @@ import crypto from "crypto"; import { KMSClient, SignCommand, SignCommandInput } from "@aws-sdk/client-kms"; +import { + ClientId, + generateId, + PurposeId, + TenantId, +} from "pagopa-interop-models"; import { SessionTokenGenerationConfig } from "../config/sessionTokenGenerationConfig.js"; import { TokenGenerationConfig } from "../config/tokenGenerationConfig.js"; +import { AuthorizationServerTokenGenerationConfig } from "../config/authorizationServerTokenGenerationConfig.js"; import { CustomClaims, + GENERATED_INTEROP_TOKEN_M2M_ROLE, + InteropApiToken, + InteropConsumerToken, + InteropJwtApiPayload, + InteropJwtConsumerPayload, InteropJwtHeader, InteropJwtPayload, InteropToken, + ORGANIZATION_ID_CLAIM, + ROLE_CLAIM, SessionClaims, SessionJwtPayload, SessionToken, @@ -22,15 +36,27 @@ export class InteropTokenGenerator { private kmsClient: KMSClient; constructor( - private config: TokenGenerationConfig & - Partial + private config: Partial & + Partial & + Partial, + kmsClient?: KMSClient ) { - this.kmsClient = new KMSClient(); + this.kmsClient = kmsClient || new KMSClient(); } public async generateInternalToken(): Promise { const currentTimestamp = Math.floor(Date.now() / 1000); + if ( + !this.config.kid || + !this.config.issuer || + !this.config.audience || + !this.config.subject || + !this.config.secondsDuration + ) { + throw Error("TokenGenerationConfig not provided or incomplete"); + } + const header: InteropJwtHeader = { alg: JWT_HEADER_ALG, use: "sig", @@ -49,11 +75,11 @@ export class InteropTokenGenerator { [JWT_ROLE_CLAIM]: JWT_INTERNAL_ROLE, }; - const serializedToken = await this.createAndSignToken( + const serializedToken = await this.createAndSignToken({ header, payload, - this.config.kid - ); + keyId: this.config.kid, + }); return { header, @@ -96,11 +122,121 @@ export class InteropTokenGenerator { ...claims, }; - const serializedToken = await this.createAndSignToken( + const serializedToken = await this.createAndSignToken({ + header, + payload, + keyId: this.config.generatedKid, + }); + + return { + header, + payload, + serialized: serializedToken, + }; + } + + public async generateInteropApiToken({ + sub, + consumerId, + }: { + sub: ClientId; + consumerId: TenantId; + }): Promise { + if ( + !this.config.generatedInteropTokenAlgorithm || + !this.config.generatedInteropTokenKid || + !this.config.generatedInteropTokenIssuer || + !this.config.generatedInteropTokenM2MAudience || + !this.config.generatedInteropTokenM2MDurationSeconds + ) { + throw Error( + "AuthorizationServerTokenGenerationConfig not provided or incomplete" + ); + } + + const currentTimestamp = Date.now(); + + const header: InteropJwtHeader = { + alg: this.config.generatedInteropTokenAlgorithm, + use: "sig", + typ: "at+jwt", + kid: this.config.generatedInteropTokenKid, + }; + + const payload: InteropJwtApiPayload = { + jti: generateId(), + iss: this.config.generatedInteropTokenIssuer, + aud: [this.config.generatedInteropTokenM2MAudience], + sub, + iat: currentTimestamp, + nbf: currentTimestamp, + exp: + currentTimestamp + + this.config.generatedInteropTokenM2MDurationSeconds * 1000, + [ORGANIZATION_ID_CLAIM]: consumerId, + [ROLE_CLAIM]: GENERATED_INTEROP_TOKEN_M2M_ROLE, + }; + + const serializedToken = await this.createAndSignToken({ + header, + payload, + keyId: this.config.generatedInteropTokenKid, + }); + + return { + header, + payload, + serialized: serializedToken, + }; + } + + public async generateInteropConsumerToken({ + sub, + audience, + purposeId, + tokenDurationInSeconds, + }: { + sub: ClientId; + audience: string[]; + purposeId: PurposeId; + tokenDurationInSeconds: number; + }): Promise { + if ( + !this.config.generatedInteropTokenAlgorithm || + !this.config.generatedInteropTokenKid || + !this.config.generatedInteropTokenIssuer || + !this.config.generatedInteropTokenM2MAudience + ) { + throw Error( + "AuthorizationServerTokenGenerationConfig not provided or incomplete" + ); + } + + const currentTimestamp = Date.now(); + + const header: InteropJwtHeader = { + alg: this.config.generatedInteropTokenAlgorithm, + use: "sig", + typ: "at+jwt", + kid: this.config.generatedInteropTokenKid, + }; + + const payload: InteropJwtConsumerPayload = { + jti: generateId(), + iss: this.config.generatedInteropTokenIssuer, + aud: audience, + sub, + iat: currentTimestamp, + nbf: currentTimestamp, + exp: currentTimestamp + tokenDurationInSeconds * 1000, + purposeId, + }; + + const serializedToken = await this.createAndSignToken({ header, payload, - this.config.generatedKid - ); + keyId: this.config.generatedInteropTokenKid, + }); return { header, @@ -109,11 +245,15 @@ export class InteropTokenGenerator { }; } - private async createAndSignToken( - header: InteropJwtHeader, - payload: InteropJwtPayload | SessionJwtPayload, - keyId: string - ): Promise { + private async createAndSignToken({ + header, + payload, + keyId, + }: { + header: InteropJwtHeader; + payload: InteropJwtPayload | SessionJwtPayload | InteropJwtConsumerPayload; + keyId: string; + }): Promise { const serializedToken = `${b64UrlEncode( JSON.stringify(header) )}.${b64UrlEncode(JSON.stringify(payload))}`; diff --git a/packages/commons/src/interop-token/models.ts b/packages/commons/src/interop-token/models.ts index 45c16a577e..fc6f0ba3f9 100644 --- a/packages/commons/src/interop-token/models.ts +++ b/packages/commons/src/interop-token/models.ts @@ -11,6 +11,9 @@ export const ORGANIZATION_EXTERNAL_ID_CLAIM = "externalId"; export const ORGANIZATION_EXTERNAL_ID_ORIGIN_CLAIM = "origin"; export const ORGANIZATION_EXTERNAL_ID_VALUE_CLAIM = "value"; export const USER_ROLES = "user-roles"; +const PURPOSE_ID_CLAIM = "purposeId"; +export const GENERATED_INTEROP_TOKEN_M2M_ROLE = "m2m"; +export const ROLE_CLAIM = "role"; export interface InteropJwtHeader { alg: string; @@ -28,6 +31,17 @@ export type InteropJwtCommonPayload = { exp: number; }; +export type InteropJwtConsumerPayload = InteropJwtCommonPayload & { + sub: string; + [PURPOSE_ID_CLAIM]: string; +}; + +export type InteropJwtApiPayload = InteropJwtCommonPayload & { + sub: string; + [ORGANIZATION_ID_CLAIM]: string; + [ROLE_CLAIM]: string; +}; + export type InteropJwtPayload = InteropJwtCommonPayload & { sub: string; role: string; @@ -39,6 +53,18 @@ export type InteropToken = { serialized: string; }; +export type InteropConsumerToken = { + header: InteropJwtHeader; + payload: InteropJwtConsumerPayload; + serialized: string; +}; + +export type InteropApiToken = { + header: InteropJwtHeader; + payload: InteropJwtApiPayload; + serialized: string; +}; + const Organization = z.object({ id: z.string(), name: z.string(), diff --git a/packages/models/src/index.ts b/packages/models/src/index.ts index 853c28d655..15525b5dfb 100644 --- a/packages/models/src/index.ts +++ b/packages/models/src/index.ts @@ -57,6 +57,7 @@ export * from "./user/user.js"; export * from "./token-generation-readmodel/platform-states-entry.js"; export * from "./token-generation-readmodel/token-generation-states-entry.js"; export * from "./token-generation-readmodel/commons.js"; +export * from "./token-generation-audit/audit.js"; // Protobuf export * from "./protobuf/protobuf.js"; diff --git a/packages/models/src/token-generation-audit/audit.ts b/packages/models/src/token-generation-audit/audit.ts new file mode 100644 index 0000000000..b3376dc26d --- /dev/null +++ b/packages/models/src/token-generation-audit/audit.ts @@ -0,0 +1,48 @@ +import { z } from "zod"; +import { + AgreementId, + ClientId, + DescriptorId, + EServiceId, + PurposeId, + PurposeVersionId, + TenantId, +} from "../brandedIds.js"; + +export const ClientAssertionAuditDetails = z.object({ + jwtId: z.string(), + issuedAt: z.number(), + algorithm: z.string(), + keyId: z.string(), + issuer: z.string(), + subject: ClientId, + audience: z.string(), + expirationTime: z.number(), +}); +export type ClientAssertionAuditDetails = z.infer< + typeof ClientAssertionAuditDetails +>; + +export const GeneratedTokenAuditDetails = z.object({ + jwtId: z.string(), + correlationId: z.string(), + issuedAt: z.number(), + clientId: ClientId, + organizationId: TenantId, + agreementId: AgreementId, + eserviceId: EServiceId, + descriptorId: DescriptorId, + purposeId: PurposeId, + purposeVersionId: PurposeVersionId, + algorithm: z.string(), + keyId: z.string(), + audience: z.string(), + subject: z.string(), + notBefore: z.number(), + expirationTime: z.number(), + issuer: z.string(), + clientAssertion: ClientAssertionAuditDetails, +}); +export type GeneratedTokenAuditDetails = z.infer< + typeof GeneratedTokenAuditDetails +>; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7d0733a96d..29496c6f9d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1557,6 +1557,9 @@ importers: dotenv-flow: specifier: 4.1.0 version: 4.1.0 + jose: + specifier: 5.9.4 + version: 5.9.4 jsonwebtoken: specifier: 9.0.2 version: 9.0.2 @@ -10844,7 +10847,6 @@ packages: /jose@5.9.4: resolution: {integrity: sha512-WBBl6au1qg6OHj67yCffCgFR3BADJBXN8MdRvCgJDuMv3driV2nHr7jdGvaKX9IolosAsn+M0XRArqLXUhyJHQ==} - dev: false /js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}