From fd645281b3058c7969cf3e4923a169869c8b5984 Mon Sep 17 00:00:00 2001 From: paologaleotti Date: Tue, 17 Sep 2024 10:39:00 +0200 Subject: [PATCH 1/4] feat: tools router scaffolding --- packages/bff/src/routers/toolsRouter.ts | 24 +++++++++++++++++++++++ packages/bff/src/services/toolsService.ts | 7 +++++++ 2 files changed, 31 insertions(+) create mode 100644 packages/bff/src/routers/toolsRouter.ts create mode 100644 packages/bff/src/services/toolsService.ts diff --git a/packages/bff/src/routers/toolsRouter.ts b/packages/bff/src/routers/toolsRouter.ts new file mode 100644 index 0000000000..c90ddce0b1 --- /dev/null +++ b/packages/bff/src/routers/toolsRouter.ts @@ -0,0 +1,24 @@ +import { ZodiosEndpointDefinitions } from "@zodios/core"; +import { ZodiosRouter } from "@zodios/express"; +import { + ExpressContext, + ZodiosContext, + zodiosValidationErrorToApiProblem, +} from "pagopa-interop-commons"; +import { bffApi } from "pagopa-interop-api-clients"; + +const toolsRouter = ( + ctx: ZodiosContext +): ZodiosRouter => { + const toolsRouter = ctx.router(bffApi.toolsApi.api, { + validationErrorHandler: zodiosValidationErrorToApiProblem, + }); + + toolsRouter.post("/tools/validateTokenGeneration", async (_req, res) => + res.status(501).send() + ); + + return toolsRouter; +}; + +export default toolsRouter; diff --git a/packages/bff/src/services/toolsService.ts b/packages/bff/src/services/toolsService.ts new file mode 100644 index 0000000000..843e344080 --- /dev/null +++ b/packages/bff/src/services/toolsService.ts @@ -0,0 +1,7 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ + +export function toolsServiceBuilder() { + return {}; +} + +export type ToolsService = ReturnType; From 59d4b44790dc9e12091b4d5bd2e42949350ae803 Mon Sep 17 00:00:00 2001 From: paologaleotti Date: Tue, 17 Sep 2024 10:40:16 +0200 Subject: [PATCH 2/4] refactor: removed unused --- packages/bff/src/routers/toolsRouter.ts | 24 ------------------------ 1 file changed, 24 deletions(-) delete mode 100644 packages/bff/src/routers/toolsRouter.ts diff --git a/packages/bff/src/routers/toolsRouter.ts b/packages/bff/src/routers/toolsRouter.ts deleted file mode 100644 index c90ddce0b1..0000000000 --- a/packages/bff/src/routers/toolsRouter.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { ZodiosEndpointDefinitions } from "@zodios/core"; -import { ZodiosRouter } from "@zodios/express"; -import { - ExpressContext, - ZodiosContext, - zodiosValidationErrorToApiProblem, -} from "pagopa-interop-commons"; -import { bffApi } from "pagopa-interop-api-clients"; - -const toolsRouter = ( - ctx: ZodiosContext -): ZodiosRouter => { - const toolsRouter = ctx.router(bffApi.toolsApi.api, { - validationErrorHandler: zodiosValidationErrorToApiProblem, - }); - - toolsRouter.post("/tools/validateTokenGeneration", async (_req, res) => - res.status(501).send() - ); - - return toolsRouter; -}; - -export default toolsRouter; From 6eaba1d3893abded53ae99b99af6f6ace7adbb40 Mon Sep 17 00:00:00 2001 From: paologaleotti Date: Fri, 20 Sep 2024 10:04:23 +0200 Subject: [PATCH 3/4] WIP --- packages/bff/package.json | 1 + packages/bff/src/providers/clientProvider.ts | 4 + packages/bff/src/services/toolsService.ts | 129 ++++++++++++++++++- pnpm-lock.yaml | 3 + 4 files changed, 135 insertions(+), 2 deletions(-) diff --git a/packages/bff/package.json b/packages/bff/package.json index 2e6c4063d5..d77b62b7e1 100644 --- a/packages/bff/package.json +++ b/packages/bff/package.json @@ -41,6 +41,7 @@ "pagopa-interop-api-clients": "workspace:*", "pagopa-interop-models": "workspace:*", "pagopa-interop-agreement-lifecycle": "workspace:*", + "pagopa-interop-client-assertion-validation": "workspace:*", "qs" : "6.12.3", "ts-pattern": "5.2.0", "uuid": "10.0.0", diff --git a/packages/bff/src/providers/clientProvider.ts b/packages/bff/src/providers/clientProvider.ts index 5a4d575ea3..810582831d 100644 --- a/packages/bff/src/providers/clientProvider.ts +++ b/packages/bff/src/providers/clientProvider.ts @@ -32,6 +32,7 @@ export type PurposeProcessClient = ReturnType< export type AuthorizationProcessClient = { client: ReturnType; user: ReturnType; + token: ReturnType; }; export type PagoPAInteropBeClients = { @@ -62,6 +63,9 @@ export function getInteropBeClients(): PagoPAInteropBeClients { authorizationClient: { client: authorizationApi.createClientApiClient(config.authorizationUrl), user: authorizationApi.createUserApiClient(config.authorizationUrl), + token: authorizationApi.createTokenGenerationApiClient( + config.authorizationUrl + ), }, }; } diff --git a/packages/bff/src/services/toolsService.ts b/packages/bff/src/services/toolsService.ts index 843e344080..56661e6a51 100644 --- a/packages/bff/src/services/toolsService.ts +++ b/packages/bff/src/services/toolsService.ts @@ -1,7 +1,132 @@ /* eslint-disable @typescript-eslint/explicit-function-return-type */ -export function toolsServiceBuilder() { - return {}; +import { + ApiKey, + ClientAssertion, + ConsumerKey, + validateClientKindAndPlatformState, + validateRequestParameters, + verifyClientAssertion, + verifyClientAssertionSignature, +} from "pagopa-interop-client-assertion-validation"; +import { + AgreementId, + ClientId, + genericInternalError, + PurposeId, + TenantId, + unsafeBrandId, +} from "pagopa-interop-models"; +import { getAllFromPaginated, WithLogger } from "pagopa-interop-commons"; +import { + agreementApi, + authorizationApi, + bffApi, + purposeApi, +} from "pagopa-interop-api-clients"; +import { + AgreementProcessClient, + PagoPAInteropBeClients, +} from "../providers/clientProvider.js"; +import { BffAppContext } from "../utilities/context.js"; + +export function toolsServiceBuilder(clients: PagoPAInteropBeClients) { + const { purposeProcessClient, authorizationClient, agreementProcessClient } = + clients; + return { + async validateTokenGeneration( + clientId: string | undefined, + clientAssertion: string, + _clientAssertionType: string, + _grantType: string, + ctx: WithLogger + ): Promise { + // TODO HANDLE ERRORS! + validateRequestParameters({ + client_assertion: clientAssertion, + client_assertion_type: "urn:ietf:params:oauth:client-assert", + grant_type: "client_credentials", + client_id: clientId, + }); + + const { data: jwt } = verifyClientAssertion(clientAssertion, clientId); + if (!jwt) { + throw genericInternalError("Invalid client assertion"); + } + + const clientWithKeys = + await authorizationClient.token.getKeyWithClientByKeyId({ + params: { + clientId: jwt.payload.sub, + keyId: jwt.header.kid, + }, + headers: ctx.headers, + }); + + const purpose = await purposeProcessClient.getPurpose({ + params: { id: jwt.payload.purposeId ?? "" }, // TODO check if not exists + headers: ctx.headers, + }); + + const agreement = await retrieveAgreement( + agreementProcessClient, + purpose.consumerId, + purpose.eserviceId, + ctx + ); + + const key = getKey(jwt, clientWithKeys, agreement, purpose); + + verifyClientAssertionSignature(clientAssertion, key); + validateClientKindAndPlatformState(key, jwt); + + return {}; + }, + }; } export type ToolsService = ReturnType; + +function getKey( + jwt: ClientAssertion, + client: authorizationApi.KeyWithClient, + agreement: agreementApi.Agreement, + purpose: purposeApi.Purpose +): ApiKey | ConsumerKey { + return { + clientId: unsafeBrandId(jwt.payload.iss), + kid: jwt.header.kid, + consumerId: unsafeBrandId(client.client.consumerId), + algorithm: "RS256", + agreementId: unsafeBrandId(agreement.id), + purposeId: unsafeBrandId(purpose.id), + }; +} + +async function retrieveAgreement( + agreementClient: AgreementProcessClient, + consumerId: string, + eserviceId: string, + ctx: WithLogger +): Promise { + const agreements = await getAllFromPaginated( + async (offset, limit) => + await agreementClient.getAgreements({ + headers: ctx.headers, + queries: { + offset, + limit, + consumersIds: [consumerId], + eservicesIds: [eserviceId], + states: [ + agreementApi.AgreementState.Values.ACTIVE, + agreementApi.AgreementState.Values.SUSPENDED, + ], + }, + }) + ); + + // TODO assert only one exists + + return agreements[0]; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1ba5868bb3..942737107f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -687,6 +687,9 @@ importers: pagopa-interop-api-clients: specifier: workspace:* version: link:../api-clients + pagopa-interop-client-assertion-validation: + specifier: workspace:* + version: link:../client-assertion-validation pagopa-interop-commons: specifier: workspace:* version: link:../commons From c708c3e3c4e0c5c1eec31586aad76a8c8326487b Mon Sep 17 00:00:00 2001 From: paologaleotti Date: Fri, 20 Sep 2024 12:08:01 +0200 Subject: [PATCH 4/4] refactor: WIP --- packages/bff/src/services/toolsService.ts | 95 ++++++++++++++--------- 1 file changed, 58 insertions(+), 37 deletions(-) diff --git a/packages/bff/src/services/toolsService.ts b/packages/bff/src/services/toolsService.ts index 56661e6a51..3997ae395e 100644 --- a/packages/bff/src/services/toolsService.ts +++ b/packages/bff/src/services/toolsService.ts @@ -12,18 +12,14 @@ import { import { AgreementId, ClientId, + EServiceId, genericInternalError, PurposeId, TenantId, unsafeBrandId, } from "pagopa-interop-models"; import { getAllFromPaginated, WithLogger } from "pagopa-interop-commons"; -import { - agreementApi, - authorizationApi, - bffApi, - purposeApi, -} from "pagopa-interop-api-clients"; +import { agreementApi, bffApi } from "pagopa-interop-api-clients"; import { AgreementProcessClient, PagoPAInteropBeClients, @@ -31,8 +27,6 @@ import { import { BffAppContext } from "../utilities/context.js"; export function toolsServiceBuilder(clients: PagoPAInteropBeClients) { - const { purposeProcessClient, authorizationClient, agreementProcessClient } = - clients; return { async validateTokenGeneration( clientId: string | undefined, @@ -54,28 +48,7 @@ export function toolsServiceBuilder(clients: PagoPAInteropBeClients) { throw genericInternalError("Invalid client assertion"); } - const clientWithKeys = - await authorizationClient.token.getKeyWithClientByKeyId({ - params: { - clientId: jwt.payload.sub, - keyId: jwt.header.kid, - }, - headers: ctx.headers, - }); - - const purpose = await purposeProcessClient.getPurpose({ - params: { id: jwt.payload.purposeId ?? "" }, // TODO check if not exists - headers: ctx.headers, - }); - - const agreement = await retrieveAgreement( - agreementProcessClient, - purpose.consumerId, - purpose.eserviceId, - ctx - ); - - const key = getKey(jwt, clientWithKeys, agreement, purpose); + const key = await retrieveKey(clients, jwt, ctx); verifyClientAssertionSignature(clientAssertion, key); validateClientKindAndPlatformState(key, jwt); @@ -87,19 +60,67 @@ export function toolsServiceBuilder(clients: PagoPAInteropBeClients) { export type ToolsService = ReturnType; -function getKey( +async function retrieveKey( + { + authorizationClient, + purposeProcessClient, + agreementProcessClient, + }: PagoPAInteropBeClients, jwt: ClientAssertion, - client: authorizationApi.KeyWithClient, - agreement: agreementApi.Agreement, - purpose: purposeApi.Purpose -): ApiKey | ConsumerKey { + ctx: WithLogger +): Promise { + const client = await authorizationClient.token.getKeyWithClientByKeyId({ + params: { + clientId: jwt.payload.sub, + keyId: jwt.header.kid, + }, + headers: ctx.headers, + }); + + const { encodedPem } = await authorizationClient.client.getClientKeyById({ + headers: ctx.headers, + params: { + clientId: client.client.id, + keyId: jwt.header.kid, + }, + }); + + const purposeId = unsafeBrandId(jwt.payload.purposeId ?? ""); // TODO check if not exists + + if (client.client.kind === "API") { + return { + clientKind: "API", + kid: jwt.header.kid, + algorithm: "RS256", + publicKey: encodedPem, + clientId: unsafeBrandId(jwt.payload.iss), + consumerId: unsafeBrandId(client.client.consumerId), + purposeId, + }; + } + + const purpose = await purposeProcessClient.getPurpose({ + params: { id: purposeId }, + headers: ctx.headers, + }); + + const agreement = await retrieveAgreement( + agreementProcessClient, + purpose.consumerId, + purpose.eserviceId, + ctx + ); + return { + clientKind: "CONSUMER", clientId: unsafeBrandId(jwt.payload.iss), kid: jwt.header.kid, - consumerId: unsafeBrandId(client.client.consumerId), algorithm: "RS256", + publicKey: encodedPem, + purposeId, + consumerId: unsafeBrandId(client.client.consumerId), agreementId: unsafeBrandId(agreement.id), - purposeId: unsafeBrandId(purpose.id), + eServiceId: unsafeBrandId(agreement.eserviceId), }; }