Skip to content

Commit

Permalink
IMN-641 getKeyWithClient in auth-process (#692)
Browse files Browse the repository at this point in the history
  • Loading branch information
rGregnanin authored Jul 9, 2024
1 parent fac8f3b commit 75dcf78
Show file tree
Hide file tree
Showing 7 changed files with 317 additions and 1 deletion.
107 changes: 107 additions & 0 deletions packages/authorization-process/open-api/authorization-service-spec.yml
Original file line number Diff line number Diff line change
Expand Up @@ -569,6 +569,43 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/Problem"
"/clients/{clientId}/keys/{keyId}/bundle":
parameters:
- $ref: "#/components/parameters/CorrelationIdHeader"
get:
security: []
tags:
- tokenGeneration
summary: Returns a key and its client by client and key identifier (kid).
description: "Given a client and key identifiers it returns the corresponding key and client, if any"
operationId: getKeyWithClientByKeyId
parameters:
- name: clientId
in: path
description: ID of the client to look up
required: true
schema:
type: string
format: uuid
- name: keyId
in: path
description: the unique identifier of the key (kid) to lookup
required: true
schema:
type: string
responses:
"200":
description: returns the corresponding key
content:
application/json:
schema:
$ref: "#/components/schemas/KeyWithClient"
"404":
description: Key not found
content:
application/json:
schema:
$ref: "#/components/schemas/Problem"
"/clients/{clientId}/users/{userId}/keys":
parameters:
- $ref: "#/components/parameters/CorrelationIdHeader"
Expand Down Expand Up @@ -884,6 +921,76 @@ components:
$ref: "#/components/schemas/Key"
required:
- keys
KeyWithClient:
type: object
properties:
key:
$ref: "#/components/schemas/JWKKey"
client:
$ref: "#/components/schemas/Client"
required:
- key
- client
JWKKey:
description: "Models a JWK"
type: object
properties:
kty:
type: string
key_ops:
type: array
items:
type: string
use:
type: string
alg:
type: string
kid:
type: string
x5u:
type: string
minLength: 1
x5t:
type: string
"x5t#S256":
type: string
x5c:
type: array
items:
type: string
crv:
type: string
x:
type: string
"y":
type: string
d:
type: string
k:
type: string
"n":
type: string
e:
type: string
p:
type: string
q:
type: string
dp:
type: string
dq:
type: string
qi:
type: string
oth:
uniqueItems: false
minItems: 1
type: array
items:
$ref: "#/components/schemas/OtherPrimeInfo"
required:
- kty
- kid
OtherPrimeInfo:
title: OtherPrimeInfo
type: object
Expand Down
2 changes: 2 additions & 0 deletions packages/authorization-process/src/model/domain/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,5 @@ export type ApiKeySeed = z.infer<typeof api.schemas.KeySeed>;
export type ApiPurposeAdditionSeed = z.infer<
typeof api.schemas.PurposeAdditionDetails
>;
export type ApiJWKKey = z.infer<typeof api.schemas.JWKKey>;
export const ApiJWKKey = api.schemas.JWKKey;
35 changes: 35 additions & 0 deletions packages/authorization-process/src/routers/AuthorizationRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
removeClientPurposeErrorMapper,
removeUserErrorMapper,
createKeysErrorMapper,
getClientKeyWithClientErrorMapper,
} from "../utilities/errorMappers.js";

const readModelService = readModelServiceBuilder(
Expand Down Expand Up @@ -389,6 +390,40 @@ const authorizationRouter = (
}
}
)
.get(
"/clients/:clientId/keys/:keyId/bundle",
authorizationMiddleware([
ADMIN_ROLE,
SECURITY_ROLE,
M2M_ROLE,
SUPPORT_ROLE,
]),
async (req, res) => {
const ctx = fromAppContext(req.ctx);
try {
const { JWKKey, client } =
await authorizationService.getKeyWithClientByKeyId({
clientId: unsafeBrandId(req.params.clientId),
kid: req.params.keyId,
logger: ctx.logger,
});
return res
.status(200)
.json({
key: JWKKey,
client: clientToApiClient({ client, showUsers: false }),
})
.end();
} catch (error) {
const errorRes = makeApiProblem(
error,
getClientKeyWithClientErrorMapper,
ctx.logger
);
return res.status(errorRes.status).json(errorRes).end();
}
}
)
.delete(
"/clients/:clientId/keys/:keyId",
authorizationMiddleware([ADMIN_ROLE, SECURITY_ROLE]),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { JsonWebKey } from "crypto";
import {
Client,
ClientId,
Expand All @@ -6,6 +7,7 @@ import {
EService,
EServiceId,
Key,
KeyWithClient,
ListResult,
Purpose,
PurposeId,
Expand Down Expand Up @@ -51,6 +53,7 @@ import {
ApiClientSeed,
ApiKeysSeed,
ApiPurposeAdditionSeed,
ApiJWKKey,
} from "../model/domain/models.js";
import {
toCreateEventClientAdded,
Expand Down Expand Up @@ -671,6 +674,36 @@ export function authorizationServiceBuilder(
}
return key;
},
async getKeyWithClientByKeyId({
clientId,
kid,
logger,
}: {
clientId: ClientId;
kid: string;
logger: Logger;
}): Promise<KeyWithClient> {
logger.info(`Getting client ${clientId} and key ${kid}`);
const client = await retrieveClient(clientId, readModelService);
const key = client.data.keys.find((key) => key.kid === kid);

if (!key) {
throw keyNotFound(kid, clientId);
}

const pemKey = decodeBase64ToPem(key.encodedPem);
const jwk: JsonWebKey = createJWK(pemKey);
const jwkKey = ApiJWKKey.parse({
...jwk,
kid: key.kid,
use: "sig",
});

return {
JWKKey: jwkKey,
client: client.data,
};
},
};
}

Expand Down
7 changes: 7 additions & 0 deletions packages/authorization-process/src/utilities/errorMappers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,3 +131,10 @@ export const getClientKeyErrorMapper = (error: ApiError<ErrorCodes>): number =>
.with("clientNotFound", "keyNotFound", () => HTTP_STATUS_NOT_FOUND)
.with("organizationNotAllowedOnClient", () => HTTP_STATUS_FORBIDDEN)
.otherwise(() => HTTP_STATUS_INTERNAL_SERVER_ERROR);

export const getClientKeyWithClientErrorMapper = (
error: ApiError<ErrorCodes>
): number =>
match(error.code)
.with("clientNotFound", "keyNotFound", () => HTTP_STATUS_NOT_FOUND)
.otherwise(() => HTTP_STATUS_INTERNAL_SERVER_ERROR);
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
/* eslint-disable @typescript-eslint/no-floating-promises */
import crypto, { JsonWebKey } from "crypto";
import {
createJWK,
decodeBase64ToPem,
genericLogger,
} from "pagopa-interop-commons";
import { Client } from "pagopa-interop-models";
import { describe, it, expect } from "vitest";
import { getMockClient, getMockKey } from "pagopa-interop-commons-test";
import { clientNotFound, keyNotFound } from "../src/model/domain/errors.js";
import { ApiJWKKey } from "../src/model/domain/models.js";
import { addOneClient, authorizationService } from "./utils.js";

describe("getKeyWithClientByKeyId", async () => {
it("should get the jwkKey with client by kid if it exists", async () => {
const key = crypto.generateKeyPairSync("rsa", {
modulusLength: 2048,
}).publicKey;

const pemKey = Buffer.from(
key.export({ type: "pkcs1", format: "pem" })
).toString("base64url");

const mockKey1 = { ...getMockKey(), encodedPem: pemKey };

const jwk: JsonWebKey = createJWK(decodeBase64ToPem(pemKey));

const mockKey2 = getMockKey();
const mockClient: Client = {
...getMockClient(),
keys: [mockKey1, mockKey2],
};
const expectedJwkKey: ApiJWKKey = {
...jwk,
kty: jwk.kty!,
kid: mockKey1.kid,
use: "sig",
};
await addOneClient(mockClient);

const { JWKKey, client } =
await authorizationService.getKeyWithClientByKeyId({
clientId: mockClient.id,
kid: mockKey1.kid,
logger: genericLogger,
});
expect(JWKKey).toEqual(expectedJwkKey);
expect(client).toEqual(mockClient);
});

it("should throw clientNotFound if the client doesn't exist", async () => {
const mockKey = getMockKey();
const mockClient: Client = {
...getMockClient(),
};

expect(
authorizationService.getKeyWithClientByKeyId({
clientId: mockClient.id,
kid: mockKey.kid,
logger: genericLogger,
})
).rejects.toThrowError(clientNotFound(mockClient.id));
});
it("should throw keyNotFound if the key doesn't exist", async () => {
const mockKey = getMockKey();
const mockClient: Client = {
...getMockClient(),
keys: [getMockKey()],
};
await addOneClient(mockClient);

expect(
authorizationService.getKeyWithClientByKeyId({
clientId: mockClient.id,
kid: mockKey.kid,
logger: genericLogger,
})
).rejects.toThrowError(keyNotFound(mockKey.kid, mockClient.id));
});
});
51 changes: 50 additions & 1 deletion packages/models/src/authorization/key.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { z } from "zod";
import { ClientId, UserId } from "../brandedIds.js";
import { ClientId, PurposeId, TenantId, UserId } from "../brandedIds.js";
import { ClientKind } from "./client.js";

export const keyUse = {
sig: "Sig",
Expand All @@ -23,3 +24,51 @@ export const Key = z.object({
});

export type Key = z.infer<typeof Key>;

export const KeyWithClient = z.object({
JWKKey: z.object({
kty: z.string(),
keyOps: z.array(z.string()).optional(),
use: z.string().optional(),
alg: z.string().optional(),
kid: z.string(),
x5u: z.string().optional(),
x5t: z.string().optional(),
x5tS256: z.string().optional(),
x5c: z.array(z.string()).optional(),
crv: z.string().optional(),
x: z.string().optional(),
y: z.string().optional(),
d: z.string().optional(),
k: z.string().optional(),
n: z.string().optional(),
e: z.string().optional(),
p: z.string().optional(),
q: z.string().optional(),
dp: z.string().optional(),
dq: z.string().optional(),
qi: z.string().optional(),
oth: z
.array(
z.object({
r: z.string(),
d: z.string(),
t: z.string(),
})
)
.optional(),
}),
client: z.object({
id: ClientId,
consumerId: TenantId,
name: z.string(),
purposes: z.array(PurposeId),
description: z.string().optional(),
users: z.array(UserId),
kind: ClientKind,
createdAt: z.coerce.date(),
keys: z.array(Key),
}),
});

export type KeyWithClient = z.infer<typeof KeyWithClient>;

0 comments on commit 75dcf78

Please sign in to comment.