diff --git a/.gitignore b/.gitignore index 6704566..f5fafa1 100644 --- a/.gitignore +++ b/.gitignore @@ -102,3 +102,6 @@ dist # TernJS port file .tern-port + +# SQLite local files +*.sqlite \ No newline at end of file diff --git a/examples/express-react-relaying-party/README.md b/examples/express-react-relaying-party/README.md index c182df1..7c590df 100644 --- a/examples/express-react-relaying-party/README.md +++ b/examples/express-react-relaying-party/README.md @@ -18,7 +18,7 @@ This project showcases the relaying party. - run this command `yarn build && yarn link` - cd into this directory, your local directory that corresponds to [this](https://github.com/italia/spid-cie-oidc-nodejs/tree/main/examples/express-react-relaying-party) - - run this command `yarn link @spid-cie-oidc-nodejs/relying-party && yarn build && yarn start` + - run this command `yarn link spid-cie-oidc && yarn build && yarn start` - this will start the relying party server on [http://127.0.0.1:3000](http://127.0.0.1:3000), keep it running - do the onboarding process diff --git a/examples/express-react-relaying-party/backend/src/index.ts b/examples/express-react-relaying-party/backend/src/index.ts index 209eb86..5163a0a 100644 --- a/examples/express-react-relaying-party/backend/src/index.ts +++ b/examples/express-react-relaying-party/backend/src/index.ts @@ -6,7 +6,7 @@ import { AgnosticRequest, AgnosticResponse, EndpointHandlers, -} from "@spid-cie-oidc-nodejs/relying-party"; +} from "spid-cie-oidc"; main(); async function main() { @@ -29,13 +29,19 @@ async function main() { } = await EndpointHandlers(configuration); function adaptRequest(req: Request): AgnosticRequest { - return { query: req.query }; + return { + url: `${req.protocol}://${req.get("host")}${req.originalUrl}`, + headers: req.headers as Record, + query: req.query, + }; } function adaptReponse(response: AgnosticResponse, res: Response) { res.status(response.status); if (response.headers) { - for (const [headerName, headerValue] of Object.entries(response.headers)) { + for (const [headerName, headerValue] of Object.entries( + response.headers + )) { res.set(headerName, headerValue); } } @@ -48,7 +54,8 @@ async function main() { app.use(session({ secret: "spid-cie-oidc-nodejs" })); app.get("/oidc/rp/providers", async (req, res) => { - const response = await providerList(); + const request = adaptRequest(req); + const response = await providerList(request); adaptReponse(response, res); }); @@ -76,7 +83,9 @@ async function main() { app.get("/oidc/rp/revocation", async (req, res) => { if (!req.session.user_info) throw new Error(); // TODO externalize session retreival - const response = await revocation(req.session.user_info as any); + const request = adaptRequest(req); + request.query.user_info = req.session.user_info; + const response = await revocation(request); req.session.destroy((error) => { if (error) throw new Error(); // TODO decide what to do with the error }); @@ -84,7 +93,8 @@ async function main() { }); app.get("/oidc/rp/.well-known/openid-federation", async (req, res) => { - const response = await entityConfiguration(); + const request = adaptRequest(req); + const response = await entityConfiguration(request); adaptReponse(response, res); }); @@ -116,6 +126,5 @@ declare module "express-session" { } } -// TODO logger as function default implementation write filesystem rotating log // TODO session (create, destroy, update) default implementation ecrypted cookie // TODO authorizationRequest access token storage default implementation in memory? diff --git a/relying-party/README.md b/relying-party/README.md index b45570e..c93598d 100644 --- a/relying-party/README.md +++ b/relying-party/README.md @@ -10,12 +10,12 @@ More detailed descriptions are provided with [JSDoc](https://jsdoc.app/about-get ### Installation -`npm install @spid-cie-oidc-nodejs/relying-party` +`npm install spid-cie-oidc` ### Usage ```typescript -import { ConfigurationFacade, EndpointHandlers } from '@spid-cie-oidc-nodejs/relying-party'; +import { ConfigurationFacade, EndpointHandlers } from 'spid-cie-oidc'; const configuration = ConfigurationFacade({ client_id: "http://127.0.0.1:3000", diff --git a/relying-party/package.json b/relying-party/package.json index b64c249..89a8f30 100644 --- a/relying-party/package.json +++ b/relying-party/package.json @@ -1,5 +1,5 @@ { - "name": "@spid-cie-oidc-nodejs/relying-party", + "name": "spid-cie-oidc", "version": "0.0.0", "description": "openid federation relying party implementation", "main": "lib/index.js", @@ -26,7 +26,9 @@ "sqlite3": "^4.0.3", "typeorm": "^0.3.0", "undici": "^4.16.0", - "uuid": "^8.3.2" + "uuid": "^8.3.2", + "winston": "^3.6.0", + "winston-daily-rotate-file": "^4.6.1" }, "devDependencies": { "@types/jest": "^27.4.1", diff --git a/relying-party/src/AccessTokenRequest.ts b/relying-party/src/AccessTokenRequest.ts index 983b9ba..e84291e 100644 --- a/relying-party/src/AccessTokenRequest.ts +++ b/relying-party/src/AccessTokenRequest.ts @@ -32,7 +32,15 @@ export async function AccessTokenRequest( client_assertion_type, client_assertion: await createJWS({ iss, sub, aud: [token_endpoint], iat, exp, jti }, jwk), }; - // TODO when doing post request ensure timeout and ssl is respected + configuration.logger("log", { + url, + method: "POST", + headers: { + "content-type": "application/x-www-form-urlencoded", + }, + body: new URLSearchParams(params).toString(), + }); + // SHOULDDO when doing post request ensure timeout and ssl is respected const response = await request(url, { method: "POST", headers: { @@ -40,13 +48,29 @@ export async function AccessTokenRequest( }, body: new URLSearchParams(params).toString(), }); + const bodyText = await response.body.text(); + const bodyJSON = JSON.parse(bodyText); if (response.statusCode !== 200) { - throw new Error(); // TODO better error reporting + configuration.logger("error", { + statusCode: response.statusCode, + headers: response.headers, + body: bodyText, + }); + throw new Error(`access token request failed ${await response.body.text()}`); + } else { + configuration.logger("log", { + statusCode: response.statusCode, + headers: response.headers, + body: bodyText, + }); } // TODO validate reponse - return (await response.body.json()) as { + const data: { id_token: string; access_token: string; refresh_token?: string; // if offline_access scope is requested - }; + } = bodyJSON; + const { id_token, access_token, refresh_token } = data; + configuration.auditLogger({ id_token, access_token, refresh_token }); + return data; } diff --git a/relying-party/src/AuthenticationRequest.ts b/relying-party/src/AuthenticationRequest.ts index df4297d..59da4a6 100644 --- a/relying-party/src/AuthenticationRequest.ts +++ b/relying-party/src/AuthenticationRequest.ts @@ -1,9 +1,16 @@ import crypto from "crypto"; -import { createJWS, generateRandomString, getPrivateJWKforProvider, makeIat } from "./utils"; +import { + BadRequestError, + createJWS, + generateRandomString, + getPrivateJWKforProvider, + isValidURL, + makeIat, +} from "./utils"; import { Configuration } from "./Configuration"; import { AuthenticationRequestEntity } from "./persistance/entity/AuthenticationRequestEntity"; import { dataSource } from "./persistance/data-source"; -import { TrustChain } from "./TrustChain"; +import { CachedTrustChain } from "./TrustChain"; export async function AuthenticationRequest( configuration: Configuration, @@ -23,16 +30,37 @@ export async function AuthenticationRequest( profile?: string; } ) { - // TODO validate provider is a known provider - // TODO validate scope parameter (should be space sperated list of supported scopes, must include openid) - // TODO validate prompt inculdes supported values (space separated) - // TODO validate redirect_uri is well formed - - const identityProviderTrustChain = await TrustChain( - configuration.client_id, - provider, - configuration.trust_anchors[0] - ); // TODO try with all anchors + if (!isValidURL(provider)) { + throw new BadRequestError(`provider is not a valid url ${provider}`); + } + if (!configuration.identity_providers.includes(provider)) { + throw new BadRequestError(`provider is not supported ${provider}`); + } + if (!isValidURL(redirect_uri)) { + throw new BadRequestError(`redirect_uri must be a valid url ${redirect_uri}`); + } + if (prompt !== "consent login") { + // SHOULDDO validate prompt inculdes supported values (space separated) and no duplicates + throw new BadRequestError(`prompt is not supported ${prompt}`); + } + if (scope !== "openid") { + // SHOULDDO validate scope parameter (should be space sperated list of supported scopes, must include openid, no duplicates) + throw new BadRequestError(`scope is not suppported ${scope}`); + } + const identityProviderTrustChain = ( + await Promise.all( + configuration.trust_anchors.map(async (trust_anchor) => { + try { + return CachedTrustChain(configuration, configuration.client_id, provider, trust_anchor); + } catch (error) { + return null; + } + }) + ) + ).find((trust_chain) => trust_chain !== null); + if (!identityProviderTrustChain) { + throw new Error(`Unable to find trust chain for identity provider ${provider}`); + } const { authorization_endpoint, token_endpoint, diff --git a/relying-party/src/Configuration.ts b/relying-party/src/Configuration.ts index 0953b7b..e233b3b 100644 --- a/relying-party/src/Configuration.ts +++ b/relying-party/src/Configuration.ts @@ -1,7 +1,7 @@ import * as jose from "jose"; -import { inferAlgForJWK, isValidEmail, isValidURL } from "./utils"; +import { inferAlgForJWK, isValidEmail, isValidURL, LogLevel } from "./utils"; import { isEqual, difference, uniq } from "lodash"; -import { UserInfo } from "./UserInfo"; +import { UserInfo } from "./UserInfoRequest"; /** * This configuration must be done on the relaying party side @@ -64,6 +64,16 @@ export type Configuration = { federation_default_exp: number; /** this function will be used to derive a user unique identifier from claims */ deriveUserIdentifier(user_info: UserInfo): string; + /** + * a function that will be called to log detailed events and exceptions + * @see {@link logRotatingFilesystem} for an example + */ + logger(level: LogLevel, message: Error | string | object | unknown): void; + /** + * a function that will be called to log mandatory details that must be stored for 24 months (such as access_token, refresh_token, id_token) + * @see {@link auditLogRotatingFilesystem} for an example + */ + auditLogger(message: object | unknown): void; }; export async function validateConfiguration(configuration: Configuration) { @@ -142,4 +152,10 @@ export async function validateConfiguration(configuration: Configuration) { ); } } + if (typeof configuration.logger !== "function") { + throw new Error(`configuration: logger must be a function`); + } + if (typeof configuration.auditLogger !== "function") { + throw new Error(`configuration: auditLogger must be a function`); + } } diff --git a/relying-party/src/ConfigurationFacade.ts b/relying-party/src/ConfigurationFacade.ts index ba638f7..9192eb9 100644 --- a/relying-party/src/ConfigurationFacade.ts +++ b/relying-party/src/ConfigurationFacade.ts @@ -1,6 +1,9 @@ -import * as fs from "fs"; -import { generateJWKS } from "./utils"; import { Configuration } from "./Configuration"; +import { auditLogRotatingFilesystem } from "./default-implementations/auditLogRotatingFilesystem"; +import { deriveFiscalNumberUserIdentifier } from "./default-implementations/deriveFiscalNumberUserIdentifier"; +import { loadOrCreateJWKSFromFilesystem } from "./default-implementations/loadOrCreateJWKSFromFilesystem"; +import { loadTrustMarksFromFilesystem } from "./default-implementations/loadTrustMarksFromFilesystem"; +import { logRotatingFilesystem } from "./default-implementations/logRotatingFilesystem"; /** * This is a configuration facade to minimize setup effort. @@ -19,8 +22,11 @@ export async function ConfigurationFacade({ trust_anchors: Array; identity_providers: Array; }): Promise { - const { public_jwks, private_jwks } = await loadOrCreateJWKS(); - const trust_marks = await loadTrustMarks(); + const { public_jwks, private_jwks } = await loadOrCreateJWKSFromFilesystem(); + const trust_marks = await loadTrustMarksFromFilesystem(); + const logger = logRotatingFilesystem; + const auditLogger = auditLogRotatingFilesystem; + const deriveUserIdentifier = deriveFiscalNumberUserIdentifier; return { client_id, client_name, @@ -77,45 +83,8 @@ export async function ConfigurationFacade({ private_jwks, trust_marks, redirect_uris: [client_id + "callback"], - deriveUserIdentifier(claims) { - const userIdentifierFields = ["https://attributes.spid.gov.it/fiscalNumber", "fiscalNumber"]; - if (!(typeof claims === "object" && claims !== null)) throw new Error(); - const claimsAsRecord = claims as Record; - for (const userIdentifierField of userIdentifierFields) { - const value = claimsAsRecord[userIdentifierField]; - if (typeof value === "string") return value; - } - throw new Error(); - }, + deriveUserIdentifier, + logger, + auditLogger, }; } - -async function loadOrCreateJWKS() { - const public_jwks_path = "./public.jwks.json"; - const private_jwks_path = "./private.jwks.json"; - if ((await fileExists(public_jwks_path)) && (await fileExists(private_jwks_path))) { - const public_jwks = JSON.parse(await fs.promises.readFile(public_jwks_path, "utf8")); - const private_jwks = JSON.parse(await fs.promises.readFile(private_jwks_path, "utf8")); - return { public_jwks, private_jwks }; - } else { - const { public_jwks, private_jwks } = await generateJWKS(); - await fs.promises.writeFile(public_jwks_path, JSON.stringify(public_jwks)); - await fs.promises.writeFile(private_jwks_path, JSON.stringify(private_jwks)); - return { public_jwks, private_jwks }; - } -} - -async function loadTrustMarks() { - const trust_marks_path = "./trust_marks.json"; - if (await fileExists(trust_marks_path)) return JSON.parse(await fs.promises.readFile(trust_marks_path, "utf8")); - else return []; -} - -async function fileExists(path: string) { - try { - await fs.promises.stat(path); - return true; - } catch (error) { - return false; - } -} diff --git a/relying-party/src/EndpointHandlers.ts b/relying-party/src/EndpointHandlers.ts index ef665bd..88999bc 100644 --- a/relying-party/src/EndpointHandlers.ts +++ b/relying-party/src/EndpointHandlers.ts @@ -6,9 +6,8 @@ import { dataSource } from "./persistance/data-source"; import { AccessTokenResponseEntity } from "./persistance/entity/AccessTokenResponseEntity"; import { AuthenticationRequestEntity } from "./persistance/entity/AuthenticationRequestEntity"; import { RevocationRequest } from "./RevocationRequest"; -import { UserInfo } from "./UserInfo"; -import { UserInfoRequest } from "./UserInfoRequest"; -import { isString, isUndefined, REPLACEME_logError } from "./utils"; +import { UserInfo, UserInfoRequest } from "./UserInfoRequest"; +import { BadRequestError, isString, isUndefined } from "./utils"; export async function EndpointHandlers(configuration: Configuration) { await validateConfiguration(configuration); @@ -18,16 +17,15 @@ export async function EndpointHandlers(configuration: Configuration) { * * used during onboarding with federation */ - async entityConfiguration(): Promise { + async entityConfiguration(request: AgnosticRequest<{}>): Promise { + configuration.logger("info", { request }); try { const jws = await EntityConfiguration(configuration); - return { - status: 200, - headers: { "Content-Type": "application/entity-statement+jwt" }, - body: jws, - }; + const response = { status: 200, headers: { "Content-Type": "application/entity-statement+jwt" }, body: jws }; + configuration.logger("info", { response }); + return response; } catch (error) { - REPLACEME_logError(error); + configuration.logger("error", error); return { status: 500 }; } }, @@ -38,16 +36,24 @@ export async function EndpointHandlers(configuration: Configuration) { * * @example login */ - async providerList(): Promise { - return { - status: 200, - headers: { "Content-Type": "application/json" }, - body: JSON.stringify( - configuration.identity_providers.map((id) => { - return { id, name: "", img: "" }; - }) - ), - }; + async providerList(request: AgnosticRequest<{}>): Promise { + configuration.logger("debug", { request }); + try { + const response = { + status: 200, + headers: { "Content-Type": "application/json" }, + body: JSON.stringify( + configuration.identity_providers.map((id) => { + return { id, name: "", img: "" }; + }) + ), + }; + configuration.logger("debug", { request }); + return response; + } catch (error) { + configuration.logger("error", error); + return { status: 500 }; + } }, /** * user lands here from a link provided in login page @@ -64,6 +70,7 @@ export async function EndpointHandlers(configuration: Configuration) { prompt?: string; }> ): Promise { + configuration.logger("info", { request }); try { const provider = request.query.provider as string; if (!isString(provider)) { @@ -92,18 +99,15 @@ export async function EndpointHandlers(configuration: Configuration) { acr_values, prompt, }); - return { - status: 302, - headers: { Location: redirectUrl }, - }; + const response = { status: 302, headers: { Location: redirectUrl } }; + configuration.logger("info", { response }); + return response; } catch (error) { if (error instanceof BadRequestError) { - return { - status: 400, - body: error.message, - }; + configuration.logger("info", { error }); + return { status: 400, body: error.message }; } else { - REPLACEME_logError(error); + configuration.logger("error", error); return { status: 500 }; } } @@ -117,6 +121,7 @@ export async function EndpointHandlers(configuration: Configuration) { async callback( request: AgnosticRequest<{ code: string; state: string } | { error: string; error_description?: string }> ): Promise { + configuration.logger("log", { request }); try { if ("error" in request.query) { if (!isString(request.query.error)) { @@ -127,11 +132,13 @@ export async function EndpointHandlers(configuration: Configuration) { } const error = request.query.error; const error_description = request.query.error_description; - return { + const response = { status: 400, headers: { "Content-Type": "application/json" }, body: JSON.stringify({ error, error_description }), }; + configuration.logger("info", { response }); + return response; } else if ("code" in request.query) { if (!isString(request.query.code)) { throw new BadRequestError("code is mandatory string parameter"); @@ -157,22 +164,16 @@ export async function EndpointHandlers(configuration: Configuration) { revoked: false, }) ); - return { - status: 200, - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(user_info), - }; + return { status: 200, headers: { "Content-Type": "application/json" }, body: JSON.stringify(user_info) }; } else { throw new BadRequestError(JSON.stringify(request.query, null, 2)); } } catch (error) { if (error instanceof BadRequestError) { - return { - status: 400, - body: error.message, - }; + configuration.logger("warn", { error }); + return { status: 400, body: error.message }; } else { - REPLACEME_logError(error); + configuration.logger("error", error); return { status: 500 }; } } @@ -180,19 +181,32 @@ export async function EndpointHandlers(configuration: Configuration) { /** * called from frontend to logout the user */ - async revocation(user_info: UserInfo): Promise { + async revocation(request: AgnosticRequest<{ user_info: UserInfo }>): Promise { + configuration.logger("log", { request }); try { - await RevocationRequest(configuration, user_info); - return { status: 200 }; + if (!request.query.user_info) { + throw new BadRequestError("user_info is mandatory parameter"); + } + await RevocationRequest(configuration, request.query.user_info); + const response = { status: 200 }; + configuration.logger("log", { response }); + return response; } catch (error) { - REPLACEME_logError(error); - return { status: 500 }; + if (error instanceof BadRequestError) { + configuration.logger("warn", { error }); + return { status: 400, body: error.message }; + } else { + configuration.logger("error", error); + return { status: 500 }; + } } }, }; } export type AgnosticRequest = { + url: string; + headers: Record; query: Query; }; @@ -201,5 +215,3 @@ export type AgnosticResponse = { headers?: Record; body?: string; }; - -class BadRequestError extends Error {} diff --git a/relying-party/src/RevocationRequest.ts b/relying-party/src/RevocationRequest.ts index 610b664..1bc8864 100644 --- a/relying-party/src/RevocationRequest.ts +++ b/relying-party/src/RevocationRequest.ts @@ -2,7 +2,7 @@ import { request } from "undici"; import { Configuration } from "./Configuration"; import { dataSource } from "./persistance/data-source"; import { AccessTokenResponseEntity } from "./persistance/entity/AccessTokenResponseEntity"; -import { UserInfo } from "./UserInfo"; +import { UserInfo } from "./UserInfoRequest"; import { createJWS, getPrivateJWKforProvider, makeExp, makeIat, makeJti } from "./utils"; export async function RevocationRequest(configuration: Configuration, user_info: UserInfo) { @@ -42,11 +42,15 @@ export async function RevocationRequest(configuration: Configuration, user_info: }; accessTokenRequestEntity.revoked = true; await dataSource.manager.save(accessTokenRequestEntity); // TODO refactor to a better places - - // fetch part - // ------------------- - - // TODO when doing post request ensure timeout and ssl is respected + configuration.logger("log", { + url, + method: "POST", + headers: { + "content-type": "application/x-www-form-urlencoded", + }, + body: new URLSearchParams(params).toString(), + }); + // SHOULDDO when doing post request ensure timeout and ssl is respected const response = await request(url, { method: "POST", headers: { @@ -54,12 +58,20 @@ export async function RevocationRequest(configuration: Configuration, user_info: }, body: new URLSearchParams(params).toString(), }); + const bodyText = await response.body.text(); if (response.statusCode !== 200) { - // console.log(response.statusCode, await response.body.json()); - // throw new Error(); // TODO + configuration.logger("warn", { + statusCode: response.statusCode, + headers: response.headers, + body: bodyText, + }); + } else { + configuration.logger("log", { + statusCode: response.statusCode, + headers: response.headers, + body: bodyText, + }); } - // TODO validate reponse } - // TODO audit log revocation request } diff --git a/relying-party/src/TrustChain.ts b/relying-party/src/TrustChain.ts index eb7d9cb..b5d262c 100644 --- a/relying-party/src/TrustChain.ts +++ b/relying-party/src/TrustChain.ts @@ -1,16 +1,22 @@ import { request } from "undici"; import * as jose from "jose"; -import { inferAlgForJWK } from "./utils"; +import { inferAlgForJWK, makeIat } from "./utils"; import { TrustAnchorEntityConfiguration, IdentityProviderEntityConfiguration, RelyingPartyEntityConfiguration, } from "./EntityConfiguration"; import { cloneDeep, difference, intersection } from "lodash"; +import { Configuration } from "./Configuration"; // SHOULDDO implement arbitray length tst chain validation -// TODO check authority hints -export async function TrustChain(relying_party: string, identity_provider: string, trust_anchor: string) { +// SHOULDDO check authority hints +async function TrustChain( + configuration: Configuration, + relying_party: string, + identity_provider: string, + trust_anchor: string +) { const relying_party_entity_configuration = await getEntityConfiguration( relying_party ); @@ -35,10 +41,18 @@ export async function TrustChain(relying_party: string, identity_provider: strin ...identity_provider_entity_configuration, metadata, }; - return { + configuration.logger("log", { + relying_party, + identity_provider, + trust_anchor, + relying_party_entity_configuration, + identity_provider_entity_configuration, + relying_party_entity_statement, + identity_provider_entity_statement, exp, - entity_configuration, - }; + metadata, + }); + return { exp, entity_configuration }; } async function getEntityStatement( @@ -68,7 +82,7 @@ async function getEntityStatement( return JSON.parse(new TextDecoder().decode(payload)); } -// TODO memoize by expiration +// SHOULDDO memoize by expiration async function getEntityConfiguration(url: string): Promise { // TODO when doing post request ensure timeout and ssl is respected const response = await request(url + ".well-known/openid-federation", { @@ -159,3 +173,22 @@ export async function verifyEntityConfiguration(jws: string) { // TODO verify schema (verify that has trust_marks) return entity_configuration; } + +const trustChainCache = new Map>>(); +export async function CachedTrustChain( + configuration: Configuration, + relying_party: string, + identity_provider: string, + trust_anchor: string +) { + const cacheKey = `${relying_party}-${identity_provider}-${trust_anchor}`; + const cached = trustChainCache.get(cacheKey); + const now = makeIat(); + if (cached && cached.exp > now) { + return cached; + } else { + const trust_chain = await TrustChain(configuration, relying_party, identity_provider, trust_anchor); + trustChainCache.set(cacheKey, trust_chain); + return trust_chain; + } +} diff --git a/relying-party/src/UserInfo.ts b/relying-party/src/UserInfo.ts deleted file mode 100644 index 703252c..0000000 --- a/relying-party/src/UserInfo.ts +++ /dev/null @@ -1 +0,0 @@ -export type UserInfo = Record; \ No newline at end of file diff --git a/relying-party/src/UserInfoRequest.ts b/relying-party/src/UserInfoRequest.ts index a9e39a0..10da7db 100644 --- a/relying-party/src/UserInfoRequest.ts +++ b/relying-party/src/UserInfoRequest.ts @@ -2,7 +2,6 @@ import * as jose from "jose"; import { request } from "undici"; import { Configuration } from "./Configuration"; import { AuthenticationRequestEntity } from "./persistance/entity/AuthenticationRequestEntity"; -import { UserInfo } from "./UserInfo"; import { inferAlgForJWK } from "./utils"; export async function UserInfoRequest( @@ -10,42 +9,62 @@ export async function UserInfoRequest( authenticationRequestEntity: AuthenticationRequestEntity, access_token: string ) { - // TODO ensure timeout and ssl is used when doing get request - const url = authenticationRequestEntity.userinfo_endpoint; - const response = await request(url, { - headers: { Authorization: `Bearer ${access_token}` }, + // SHOULDDO ensure timeout and ssl is used when doing get request + const url = authenticationRequestEntity.userinfo_endpoint; + configuration.logger("log", { + url, + method: "GET", + headers: { Authorization: `Bearer ${access_token}` }, + }); + const response = await request(url, { + headers: { Authorization: `Bearer ${access_token}` }, + }); + const bodyText = await response.body.text(); + if (response.statusCode !== 200) { + configuration.logger("error", { + statusCode: response.statusCode, + headers: response.headers, + body: bodyText, + }); + throw new Error(`user info request failed`); + } else { + configuration.logger("log", { + statusCode: response.statusCode, + headers: response.headers, + body: bodyText, }); - if (response.statusCode !== 200) throw new Error(); // TODO - const jwe = await response.body.text(); - const jws = await decrypt(configuration, jwe); - const jwt = await verify(authenticationRequestEntity, jws); - return jwt as UserInfo // TODO validate; + } + const jwe = await bodyText; + const jws = await decrypt(configuration, jwe); + const jwt = await verify(authenticationRequestEntity, jws); + return jwt as unknown as UserInfo; // TODO validate; } async function decrypt(configuration: Configuration, jwe: string) { const { plaintext } = await jose.compactDecrypt(jwe, async (header) => { if (!header.kid) throw new Error("missing kid in header"); // TODO better error report - const jwk = configuration.private_jwks.keys.find( - (key) => key.kid === header.kid - ); + const jwk = configuration.private_jwks.keys.find((key) => key.kid === header.kid); if (!jwk) throw new Error("no matching key with kid found"); // TODO better error report return await jose.importJWK(jwk, inferAlgForJWK(jwk)); }); return new TextDecoder().decode(plaintext); } -async function verify( - authenticationRequestEntity: AuthenticationRequestEntity, - jws: string -) { - return jose.decodeJwt(jws); // TODO remove this line when te jws can be correctly verified - const { payload } = await jose.compactVerify(jws, async (header) => { - if (!header.kid) throw new Error("missing kid in header"); // TODO better error report - const jwk = authenticationRequestEntity.provider_jwks.keys.find( - (key) => key.kid === header.kid - ); - if (!jwk) throw new Error("no matching key with kid found"); // TODO better error report - return await jose.importJWK(jwk, inferAlgForJWK(jwk)); - }); - return new TextDecoder().decode(payload); +async function verify(authenticationRequestEntity: AuthenticationRequestEntity, jws: string) { + try { + const { payload } = await jose.compactVerify(jws, async (header) => { + if (!header.kid) throw new Error("missing kid in header"); // TODO better error report + const jwk = authenticationRequestEntity.provider_jwks.keys.find((key) => key.kid === header.kid); + if (!jwk) throw new Error("no matching key with kid found"); // TODO better error report + return await jose.importJWK(jwk, inferAlgForJWK(jwk)); + }); + return new TextDecoder().decode(payload); + } catch (error) { + // user info jwt verificatrion failed, this should not happen + // TODO check if resolved + // TODO file issue upstream + return jose.decodeJwt(jws); + } } + +export type UserInfo = Record; diff --git a/relying-party/src/default-implementations/auditLogRotatingFilesystem.ts b/relying-party/src/default-implementations/auditLogRotatingFilesystem.ts new file mode 100644 index 0000000..2f91bf3 --- /dev/null +++ b/relying-party/src/default-implementations/auditLogRotatingFilesystem.ts @@ -0,0 +1,23 @@ +import * as winston from "winston"; +import "winston-daily-rotate-file"; + +const logger = winston.createLogger({ + format: winston.format.combine( + winston.format.metadata(), + winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), + winston.format.json() + ), + transports: [ + new winston.transports.DailyRotateFile({ + dirname: "logs", + filename: "audit-%DATE%.log", + datePattern: "YYYY-MM-DD-HH", + zippedArchive: true, + maxSize: "20m", + }), + ], +}); + +export function auditLogRotatingFilesystem(message: unknown) { + logger.log(message as any); +} diff --git a/relying-party/src/default-implementations/deriveFiscalNumberUserIdentifier.ts b/relying-party/src/default-implementations/deriveFiscalNumberUserIdentifier.ts new file mode 100644 index 0000000..1ed46a9 --- /dev/null +++ b/relying-party/src/default-implementations/deriveFiscalNumberUserIdentifier.ts @@ -0,0 +1,12 @@ +import { UserInfo } from "../UserInfoRequest"; + +export function deriveFiscalNumberUserIdentifier(user_info: UserInfo) { + const userIdentifierFields = ["https://attributes.spid.gov.it/fiscalNumber", "fiscalNumber"]; + if (!(typeof user_info === "object" && user_info !== null)) throw new Error(); + const claimsAsRecord = user_info as Record; + for (const userIdentifierField of userIdentifierFields) { + const value = claimsAsRecord[userIdentifierField]; + if (typeof value === "string") return value; + } + throw new Error(); +} diff --git a/relying-party/src/default-implementations/loadOrCreateJWKSFromFilesystem.ts b/relying-party/src/default-implementations/loadOrCreateJWKSFromFilesystem.ts new file mode 100644 index 0000000..71a002c --- /dev/null +++ b/relying-party/src/default-implementations/loadOrCreateJWKSFromFilesystem.ts @@ -0,0 +1,17 @@ +import * as fs from "fs"; +import { fileExists, generateJWKS } from "../utils"; + +export async function loadOrCreateJWKSFromFilesystem() { + const public_jwks_path = "./public.jwks.json"; + const private_jwks_path = "./private.jwks.json"; + if ((await fileExists(public_jwks_path)) && (await fileExists(private_jwks_path))) { + const public_jwks = JSON.parse(await fs.promises.readFile(public_jwks_path, "utf8")); + const private_jwks = JSON.parse(await fs.promises.readFile(private_jwks_path, "utf8")); + return { public_jwks, private_jwks }; + } else { + const { public_jwks, private_jwks } = await generateJWKS(); + await fs.promises.writeFile(public_jwks_path, JSON.stringify(public_jwks)); + await fs.promises.writeFile(private_jwks_path, JSON.stringify(private_jwks)); + return { public_jwks, private_jwks }; + } +} diff --git a/relying-party/src/default-implementations/loadTrustMarksFromFilesystem.ts b/relying-party/src/default-implementations/loadTrustMarksFromFilesystem.ts new file mode 100644 index 0000000..3b87d8d --- /dev/null +++ b/relying-party/src/default-implementations/loadTrustMarksFromFilesystem.ts @@ -0,0 +1,8 @@ +import * as fs from "fs"; +import { fileExists } from "../utils"; + +export async function loadTrustMarksFromFilesystem() { + const trust_marks_path = "./trust_marks.json"; + if (await fileExists(trust_marks_path)) return JSON.parse(await fs.promises.readFile(trust_marks_path, "utf8")); + else return []; +} diff --git a/relying-party/src/default-implementations/logRotatingFilesystem.ts b/relying-party/src/default-implementations/logRotatingFilesystem.ts new file mode 100644 index 0000000..b856520 --- /dev/null +++ b/relying-party/src/default-implementations/logRotatingFilesystem.ts @@ -0,0 +1,25 @@ +import * as winston from "winston"; +import { LogLevel } from "../utils"; +import "winston-daily-rotate-file"; + +const logger = winston.createLogger({ + format: winston.format.combine( + winston.format.errors({ stack: true }), + winston.format.metadata(), + winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), + winston.format.json() + ), + transports: [ + new winston.transports.DailyRotateFile({ + dirname: "logs", + filename: "log-%DATE%.log", + datePattern: "YYYY-MM-DD-HH", + zippedArchive: true, + maxSize: "20m", + }), + ], +}); + +export function logRotatingFilesystem(level: LogLevel, message: Error | string | object) { + logger[level](message as any); +} diff --git a/relying-party/src/index.ts b/relying-party/src/index.ts index ee890b1..3626128 100644 --- a/relying-party/src/index.ts +++ b/relying-party/src/index.ts @@ -2,3 +2,9 @@ export { Configuration } from "./Configuration"; export { ConfigurationFacade } from "./ConfigurationFacade"; export { generateJWKS } from "./utils"; export { AgnosticRequest, AgnosticResponse, EndpointHandlers } from "./EndpointHandlers"; +export { UserInfo } from "./UserInfoRequest"; +export { loadOrCreateJWKSFromFilesystem } from "./default-implementations/loadOrCreateJWKSFromFilesystem"; +export { loadTrustMarksFromFilesystem } from "./default-implementations/loadTrustMarksFromFilesystem"; +export { logRotatingFilesystem } from "./default-implementations/logRotatingFilesystem"; +export { auditLogRotatingFilesystem } from "./default-implementations/auditLogRotatingFilesystem"; +export { deriveFiscalNumberUserIdentifier } from "./default-implementations/deriveFiscalNumberUserIdentifier"; diff --git a/relying-party/src/test/flow.test.ts b/relying-party/src/test/flow.test.ts index 01d7dfb..ba44314 100644 --- a/relying-party/src/test/flow.test.ts +++ b/relying-party/src/test/flow.test.ts @@ -18,7 +18,7 @@ describe("test whole flow happy path", () => { test("entity configuration endpoint happy path", async () => { const { configuration, handlers } = await machinery; const { entityConfiguration } = handlers; - const response = await entityConfiguration(); + const response = await entityConfiguration({ url: "", query: {}, headers: {} }); expect(response.status).toBe(200); const entity_configuration = await verifyEntityConfiguration(response.body as string); expect(withoutFields(entity_configuration, ["iat", "exp"])).toEqual({ @@ -46,7 +46,7 @@ describe("test whole flow happy path", () => { test("provider list endpoint happy path", async () => { const { handlers } = await machinery; const { providerList } = handlers; - const response = await providerList(); + const response = await providerList({ url: "", headers: {}, query: {} }); expect(response).toEqual({ status: 200, headers: { diff --git a/relying-party/src/utils.ts b/relying-party/src/utils.ts index f461446..7abdc14 100644 --- a/relying-party/src/utils.ts +++ b/relying-party/src/utils.ts @@ -1,4 +1,5 @@ import crypto from "crypto"; +import * as fs from "fs"; import * as jose from "jose"; import * as uuid from "uuid"; import { Configuration } from "./Configuration"; @@ -37,7 +38,7 @@ export function getPrivateJWKforProvider(configuration: Configuration) { export function inferAlgForJWK(jwk: jose.JWK) { if (jwk.kty === "RSA") return "RS256"; if (jwk.kty === "EC") return "ES256"; - // TODO support more types + // SHOULDDO support more types throw new Error("unsupported key type"); } @@ -77,8 +78,15 @@ export function isUndefined(value: unknown): value is undefined { return value === undefined; } -// TODO do something with them -export function REPLACEME_logError(error: unknown) {} +export async function fileExists(path: string) { + try { + await fs.promises.stat(path); + return true; + } catch (error) { + return false; + } +} + +export type LogLevel = "error" | "warn" | "log" | "info" | "debug"; -// TODO do something with them -export function REPLACEME_logAudit(rquestOrResponse: unknown) {} \ No newline at end of file +export class BadRequestError extends Error {} diff --git a/relying-party/yarn.lock b/relying-party/yarn.lock index 38ae648..f30d87f 100644 --- a/relying-party/yarn.lock +++ b/relying-party/yarn.lock @@ -293,6 +293,20 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== +"@colors/colors@1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" + integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ== + +"@dabh/diagnostics@^2.0.2": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@dabh/diagnostics/-/diagnostics-2.0.3.tgz#7f7e97ee9a725dffc7808d93668cc984e1dc477a" + integrity sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA== + dependencies: + colorspace "1.1.x" + enabled "2.0.x" + kuler "^2.0.0" + "@istanbuljs/load-nyc-config@^1.0.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" @@ -765,6 +779,11 @@ argparse@^2.0.1: resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== +async@^3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.3.tgz#ac53dafd3f4720ee9e8a160628f18ea91df196c9" + integrity sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g== + asynckit@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" @@ -992,7 +1011,7 @@ collect-v8-coverage@^1.0.0: resolved "https://registry.yarnpkg.com/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz#cc2c8e94fc18bbdffe64d6534570c8a673b27f59" integrity sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg== -color-convert@^1.9.0: +color-convert@^1.9.0, color-convert@^1.9.3: version "1.9.3" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== @@ -1011,11 +1030,35 @@ color-name@1.1.3: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= -color-name@~1.1.4: +color-name@^1.0.0, color-name@~1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== +color-string@^1.6.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.9.0.tgz#63b6ebd1bec11999d1df3a79a7569451ac2be8aa" + integrity sha512-9Mrz2AQLefkH1UvASKj6v6hj/7eWgjnT/cVsR8CumieLoT+g900exWeNogqtweI8dxloXN9BDQTYro1oWu/5CQ== + dependencies: + color-name "^1.0.0" + simple-swizzle "^0.2.2" + +color@^3.1.3: + version "3.2.1" + resolved "https://registry.yarnpkg.com/color/-/color-3.2.1.tgz#3544dc198caf4490c3ecc9a790b54fe9ff45e164" + integrity sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA== + dependencies: + color-convert "^1.9.3" + color-string "^1.6.0" + +colorspace@1.1.x: + version "1.1.4" + resolved "https://registry.yarnpkg.com/colorspace/-/colorspace-1.1.4.tgz#8d442d1186152f60453bf8070cd66eb364e59243" + integrity sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w== + dependencies: + color "^3.1.3" + text-hex "1.0.x" + combined-stream@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" @@ -1181,6 +1224,11 @@ emoji-regex@^8.0.0: resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== +enabled@2.0.x: + version "2.0.0" + resolved "https://registry.yarnpkg.com/enabled/-/enabled-2.0.0.tgz#f9dd92ec2d6f4bbc0d5d1e64e21d61cd4665e7c2" + integrity sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ== + error-ex@^1.3.1: version "1.3.2" resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" @@ -1282,6 +1330,18 @@ fb-watchman@^2.0.0: dependencies: bser "2.1.1" +fecha@^4.2.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/fecha/-/fecha-4.2.1.tgz#0a83ad8f86ef62a091e22bb5a039cd03d23eecce" + integrity sha512-MMMQ0ludy/nBs1/o0zVOiKTpG7qMbonKUzjJgQFEuvq6INZ1OraKPRAWkBq5vlKLOUMpmNYG1JoN3oDPUQ9m3Q== + +file-stream-rotator@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/file-stream-rotator/-/file-stream-rotator-0.6.1.tgz#007019e735b262bb6c6f0197e58e5c87cb96cec3" + integrity sha512-u+dBid4PvZw17PmDeRcNOtCP9CCK/9lRN2w+r1xIS7yOL9JFrIBKTvrYsxT4P0pGtThYTn++QS5ChHaUov3+zQ== + dependencies: + moment "^2.29.1" + fill-range@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" @@ -1314,6 +1374,11 @@ find-up@^5.0.0: locate-path "^6.0.0" path-exists "^4.0.0" +fn.name@1.x.x: + version "1.1.0" + resolved "https://registry.yarnpkg.com/fn.name/-/fn.name-1.1.0.tgz#26cad8017967aea8731bc42961d04a3d5988accc" + integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw== + form-data@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.1.tgz#ebd53791b78356a99af9a300d4282c4d5eb9755f" @@ -1511,7 +1576,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@^2.0.1, inherits@~2.0.3: +inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -1526,6 +1591,11 @@ is-arrayish@^0.2.1: resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= +is-arrayish@^0.3.1: + version "0.3.2" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03" + integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ== + is-core-module@^2.8.1: version "2.8.1" resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.8.1.tgz#f59fdfca701d5879d0a6b100a40aa1560ce27211" @@ -2114,6 +2184,11 @@ kleur@^3.0.3: resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== +kuler@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/kuler/-/kuler-2.0.0.tgz#e2c570a3800388fb44407e851531c1d670b061b3" + integrity sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A== + leven@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" @@ -2156,6 +2231,17 @@ lodash@^4.17.21, lodash@^4.7.0: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== +logform@^2.3.2, logform@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/logform/-/logform-2.4.0.tgz#131651715a17d50f09c2a2c1a524ff1a4164bcfe" + integrity sha512-CPSJw4ftjf517EhXZGGvTHHkYobo7ZCc0kvwUoOYcjfR2UVrI66RHj8MCrfAdEitdmFqbu2BYdYs8FHHZSb6iw== + dependencies: + "@colors/colors" "1.5.0" + fecha "^4.2.0" + ms "^2.1.1" + safe-stable-stringify "^2.3.1" + triple-beam "^1.3.0" + lru-cache@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" @@ -2258,6 +2344,11 @@ mkdirp@^1.0.4: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== +moment@^2.29.1: + version "2.29.1" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3" + integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ== + ms@2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" @@ -2388,6 +2479,11 @@ object-assign@^4.0.1, object-assign@^4.1.0: resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= +object-hash@^2.0.1: + version "2.2.0" + resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-2.2.0.tgz#5ad518581eefc443bd763472b8ff2e9c2c0d54a5" + integrity sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw== + once@^1.3.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" @@ -2395,6 +2491,13 @@ once@^1.3.0: dependencies: wrappy "1" +one-time@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/one-time/-/one-time-1.0.0.tgz#e06bc174aed214ed58edede573b433bbf827cb45" + integrity sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g== + dependencies: + fn.name "1.x.x" + onetime@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" @@ -2620,6 +2723,15 @@ readable-stream@^2.0.6: string_decoder "~1.1.1" util-deprecate "~1.0.1" +readable-stream@^3.4.0, readable-stream@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" + integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + reflect-metadata@^0.1.13: version "0.1.13" resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.13.tgz#67ae3ca57c972a2aa1642b10fe363fe32d49dc08" @@ -2698,7 +2810,7 @@ rollup@^2.70.1: optionalDependencies: fsevents "~2.3.2" -safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@^5.2.1: +safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@^5.2.1, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== @@ -2708,6 +2820,11 @@ safe-buffer@~5.1.0, safe-buffer@~5.1.1: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== +safe-stable-stringify@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.3.1.tgz#ab67cbe1fe7d40603ca641c5e765cb942d04fc73" + integrity sha512-kYBSfT+troD9cDA85VDnHZ1rpHC50O0g1e6WlGHVCz/g+JS+9WKLj+XwFYyR8UbrZN8ll9HUpDAAddY58MGisg== + "safer-buffer@>= 2.1.2 < 3": version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" @@ -2772,6 +2889,13 @@ signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3: resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== +simple-swizzle@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a" + integrity sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo= + dependencies: + is-arrayish "^0.3.1" + sisteransi@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" @@ -2823,6 +2947,11 @@ sqlite3@^4.0.3: nan "^2.12.1" node-pre-gyp "^0.11.0" +stack-trace@0.0.x: + version "0.0.10" + resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0" + integrity sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA= + stack-utils@^2.0.3: version "2.0.5" resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.5.tgz#d25265fca995154659dbbfba3b49254778d2fdd5" @@ -2856,6 +2985,13 @@ string-width@^1.0.1: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.1" +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + string_decoder@~1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" @@ -2966,6 +3102,11 @@ test-exclude@^6.0.0: glob "^7.1.4" minimatch "^3.0.4" +text-hex@1.0.x: + version "1.0.0" + resolved "https://registry.yarnpkg.com/text-hex/-/text-hex-1.0.0.tgz#69dc9c1b17446ee79a92bf5b884bb4b9127506f5" + integrity sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg== + thenify-all@^1.0.0: version "1.6.0" resolved "https://registry.yarnpkg.com/thenify-all/-/thenify-all-1.6.0.tgz#1a1918d402d8fc3f98fbf234db0bcc8cc10e9726" @@ -3018,6 +3159,11 @@ tr46@^2.1.0: dependencies: punycode "^2.1.1" +triple-beam@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/triple-beam/-/triple-beam-1.3.0.tgz#a595214c7298db8339eeeee083e4d10bd8cb8dd9" + integrity sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw== + ts-jest@^27.1.4: version "27.1.4" resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-27.1.4.tgz#84d42cf0f4e7157a52e7c64b1492c46330943e00" @@ -3113,7 +3259,7 @@ upath2@^3.1.12: path-strip-sep "^1.0.10" tslib "^2.3.1" -util-deprecate@~1.0.1: +util-deprecate@^1.0.1, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= @@ -3198,6 +3344,41 @@ wide-align@^1.1.0: dependencies: string-width "^1.0.2 || 2 || 3 || 4" +winston-daily-rotate-file@^4.6.1: + version "4.6.1" + resolved "https://registry.yarnpkg.com/winston-daily-rotate-file/-/winston-daily-rotate-file-4.6.1.tgz#35c9db5669c381ed32acdb5849de69ab046a7a71" + integrity sha512-Ycch4LZmTycbhgiI2eQXBKI1pKcEQgAqmBjyq7/dC6Dk77nasdxvhLKraqTdCw7wNDSs8/M0jXaLATHquG7xYg== + dependencies: + file-stream-rotator "^0.6.1" + object-hash "^2.0.1" + triple-beam "^1.3.0" + winston-transport "^4.4.0" + +winston-transport@^4.4.0, winston-transport@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/winston-transport/-/winston-transport-4.5.0.tgz#6e7b0dd04d393171ed5e4e4905db265f7ab384fa" + integrity sha512-YpZzcUzBedhlTAfJg6vJDlyEai/IFMIVcaEZZyl3UXIl4gmqRpU7AE89AHLkbzLUsv0NVmw7ts+iztqKxxPW1Q== + dependencies: + logform "^2.3.2" + readable-stream "^3.6.0" + triple-beam "^1.3.0" + +winston@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/winston/-/winston-3.6.0.tgz#be32587a099a292b88c49fac6fa529d478d93fb6" + integrity sha512-9j8T75p+bcN6D00sF/zjFVmPp+t8KMPB1MzbbzYjeN9VWxdsYnTB40TkbNUEXAmILEfChMvAMgidlX64OG3p6w== + dependencies: + "@dabh/diagnostics" "^2.0.2" + async "^3.2.3" + is-stream "^2.0.0" + logform "^2.4.0" + one-time "^1.0.0" + readable-stream "^3.4.0" + safe-stable-stringify "^2.3.1" + stack-trace "0.0.x" + triple-beam "^1.3.0" + winston-transport "^4.5.0" + word-wrap@~1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"