Skip to content

Commit f51ab4b

Browse files
committed
feat: add logging
1 parent 8d5f659 commit f51ab4b

23 files changed

+584
-178
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,3 +102,6 @@ dist
102102

103103
# TernJS port file
104104
.tern-port
105+
106+
# SQLite local files
107+
*.sqlite

examples/express-react-relaying-party/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ This project showcases the relaying party.
1818
- run this command `yarn build && yarn link`
1919

2020
- 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)
21-
- run this command `yarn link @spid-cie-oidc-nodejs/relying-party && yarn build && yarn start`
21+
- run this command `yarn link spid-cie-oidc && yarn build && yarn start`
2222
- this will start the relying party server on [http://127.0.0.1:3000](http://127.0.0.1:3000), keep it running
2323

2424
- do the onboarding process

examples/express-react-relaying-party/backend/src/index.ts

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
AgnosticRequest,
77
AgnosticResponse,
88
EndpointHandlers,
9-
} from "@spid-cie-oidc-nodejs/relying-party";
9+
} from "spid-cie-oidc";
1010

1111
main();
1212
async function main() {
@@ -29,13 +29,19 @@ async function main() {
2929
} = await EndpointHandlers(configuration);
3030

3131
function adaptRequest(req: Request): AgnosticRequest<any> {
32-
return { query: req.query };
32+
return {
33+
url: `${req.protocol}://${req.get("host")}${req.originalUrl}`,
34+
headers: req.headers as Record<string, string>,
35+
query: req.query,
36+
};
3337
}
3438

3539
function adaptReponse(response: AgnosticResponse, res: Response) {
3640
res.status(response.status);
3741
if (response.headers) {
38-
for (const [headerName, headerValue] of Object.entries(response.headers)) {
42+
for (const [headerName, headerValue] of Object.entries(
43+
response.headers
44+
)) {
3945
res.set(headerName, headerValue);
4046
}
4147
}
@@ -48,7 +54,8 @@ async function main() {
4854
app.use(session({ secret: "spid-cie-oidc-nodejs" }));
4955

5056
app.get("/oidc/rp/providers", async (req, res) => {
51-
const response = await providerList();
57+
const request = adaptRequest(req);
58+
const response = await providerList(request);
5259
adaptReponse(response, res);
5360
});
5461

@@ -76,15 +83,18 @@ async function main() {
7683

7784
app.get("/oidc/rp/revocation", async (req, res) => {
7885
if (!req.session.user_info) throw new Error(); // TODO externalize session retreival
79-
const response = await revocation(req.session.user_info as any);
86+
const request = adaptRequest(req);
87+
request.query.user_info = req.session.user_info;
88+
const response = await revocation(request);
8089
req.session.destroy((error) => {
8190
if (error) throw new Error(); // TODO decide what to do with the error
8291
});
8392
adaptReponse(response, res);
8493
});
8594

8695
app.get("/oidc/rp/.well-known/openid-federation", async (req, res) => {
87-
const response = await entityConfiguration();
96+
const request = adaptRequest(req);
97+
const response = await entityConfiguration(request);
8898
adaptReponse(response, res);
8999
});
90100

@@ -116,6 +126,5 @@ declare module "express-session" {
116126
}
117127
}
118128

119-
// TODO logger as function default implementation write filesystem rotating log
120129
// TODO session (create, destroy, update) default implementation ecrypted cookie
121130
// TODO authorizationRequest access token storage default implementation in memory?

relying-party/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,12 @@ More detailed descriptions are provided with [JSDoc](https://jsdoc.app/about-get
1010

1111
### Installation
1212

13-
`npm install @spid-cie-oidc-nodejs/relying-party`
13+
`npm install spid-cie-oidc`
1414

1515
### Usage
1616

1717
```typescript
18-
import { ConfigurationFacade, EndpointHandlers } from '@spid-cie-oidc-nodejs/relying-party';
18+
import { ConfigurationFacade, EndpointHandlers } from 'spid-cie-oidc';
1919

2020
const configuration = ConfigurationFacade({
2121
client_id: "http://127.0.0.1:3000",

relying-party/package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"name": "@spid-cie-oidc-nodejs/relying-party",
2+
"name": "spid-cie-oidc",
33
"version": "0.0.0",
44
"description": "openid federation relying party implementation",
55
"main": "lib/index.js",
@@ -26,7 +26,9 @@
2626
"sqlite3": "^4.0.3",
2727
"typeorm": "^0.3.0",
2828
"undici": "^4.16.0",
29-
"uuid": "^8.3.2"
29+
"uuid": "^8.3.2",
30+
"winston": "^3.6.0",
31+
"winston-daily-rotate-file": "^4.6.1"
3032
},
3133
"devDependencies": {
3234
"@types/jest": "^27.4.1",

relying-party/src/AccessTokenRequest.ts

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,21 +32,45 @@ export async function AccessTokenRequest(
3232
client_assertion_type,
3333
client_assertion: await createJWS({ iss, sub, aud: [token_endpoint], iat, exp, jti }, jwk),
3434
};
35-
// TODO when doing post request ensure timeout and ssl is respected
35+
configuration.logger("log", {
36+
url,
37+
method: "POST",
38+
headers: {
39+
"content-type": "application/x-www-form-urlencoded",
40+
},
41+
body: new URLSearchParams(params).toString(),
42+
});
43+
// SHOULDDO when doing post request ensure timeout and ssl is respected
3644
const response = await request(url, {
3745
method: "POST",
3846
headers: {
3947
"content-type": "application/x-www-form-urlencoded",
4048
},
4149
body: new URLSearchParams(params).toString(),
4250
});
51+
const bodyText = await response.body.text();
52+
const bodyJSON = JSON.parse(bodyText);
4353
if (response.statusCode !== 200) {
44-
throw new Error(); // TODO better error reporting
54+
configuration.logger("error", {
55+
statusCode: response.statusCode,
56+
headers: response.headers,
57+
body: bodyText,
58+
});
59+
throw new Error(`access token request failed ${await response.body.text()}`);
60+
} else {
61+
configuration.logger("log", {
62+
statusCode: response.statusCode,
63+
headers: response.headers,
64+
body: bodyText,
65+
});
4566
}
4667
// TODO validate reponse
47-
return (await response.body.json()) as {
68+
const data: {
4869
id_token: string;
4970
access_token: string;
5071
refresh_token?: string; // if offline_access scope is requested
51-
};
72+
} = bodyJSON;
73+
const { id_token, access_token, refresh_token } = data;
74+
configuration.auditLogger({ id_token, access_token, refresh_token });
75+
return data;
5276
}

relying-party/src/AuthenticationRequest.ts

Lines changed: 40 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
import crypto from "crypto";
2-
import { createJWS, generateRandomString, getPrivateJWKforProvider, makeIat } from "./utils";
2+
import {
3+
BadRequestError,
4+
createJWS,
5+
generateRandomString,
6+
getPrivateJWKforProvider,
7+
isValidURL,
8+
makeIat,
9+
} from "./utils";
310
import { Configuration } from "./Configuration";
411
import { AuthenticationRequestEntity } from "./persistance/entity/AuthenticationRequestEntity";
512
import { dataSource } from "./persistance/data-source";
6-
import { TrustChain } from "./TrustChain";
13+
import { CachedTrustChain } from "./TrustChain";
714

815
export async function AuthenticationRequest(
916
configuration: Configuration,
@@ -23,16 +30,37 @@ export async function AuthenticationRequest(
2330
profile?: string;
2431
}
2532
) {
26-
// TODO validate provider is a known provider
27-
// TODO validate scope parameter (should be space sperated list of supported scopes, must include openid)
28-
// TODO validate prompt inculdes supported values (space separated)
29-
// TODO validate redirect_uri is well formed
30-
31-
const identityProviderTrustChain = await TrustChain(
32-
configuration.client_id,
33-
provider,
34-
configuration.trust_anchors[0]
35-
); // TODO try with all anchors
33+
if (!isValidURL(provider)) {
34+
throw new BadRequestError(`provider is not a valid url ${provider}`);
35+
}
36+
if (!configuration.identity_providers.includes(provider)) {
37+
throw new BadRequestError(`provider is not supported ${provider}`);
38+
}
39+
if (!isValidURL(redirect_uri)) {
40+
throw new BadRequestError(`redirect_uri must be a valid url ${redirect_uri}`);
41+
}
42+
if (prompt !== "consent login") {
43+
// SHOULDDO validate prompt inculdes supported values (space separated) and no duplicates
44+
throw new BadRequestError(`prompt is not supported ${prompt}`);
45+
}
46+
if (scope !== "openid") {
47+
// SHOULDDO validate scope parameter (should be space sperated list of supported scopes, must include openid, no duplicates)
48+
throw new BadRequestError(`scope is not suppported ${scope}`);
49+
}
50+
const identityProviderTrustChain = (
51+
await Promise.all(
52+
configuration.trust_anchors.map(async (trust_anchor) => {
53+
try {
54+
return CachedTrustChain(configuration, configuration.client_id, provider, trust_anchor);
55+
} catch (error) {
56+
return null;
57+
}
58+
})
59+
)
60+
).find((trust_chain) => trust_chain !== null);
61+
if (!identityProviderTrustChain) {
62+
throw new Error(`Unable to find trust chain for identity provider ${provider}`);
63+
}
3664
const {
3765
authorization_endpoint,
3866
token_endpoint,

relying-party/src/Configuration.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as jose from "jose";
2-
import { inferAlgForJWK, isValidEmail, isValidURL } from "./utils";
2+
import { inferAlgForJWK, isValidEmail, isValidURL, LogLevel } from "./utils";
33
import { isEqual, difference, uniq } from "lodash";
4-
import { UserInfo } from "./UserInfo";
4+
import { UserInfo } from "./UserInfoRequest";
55

66
/**
77
* This configuration must be done on the relaying party side
@@ -64,6 +64,16 @@ export type Configuration = {
6464
federation_default_exp: number;
6565
/** this function will be used to derive a user unique identifier from claims */
6666
deriveUserIdentifier(user_info: UserInfo): string;
67+
/**
68+
* a function that will be called to log detailed events and exceptions
69+
* @see {@link logRotatingFilesystem} for an example
70+
*/
71+
logger(level: LogLevel, message: Error | string | object | unknown): void;
72+
/**
73+
* 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)
74+
* @see {@link auditLogRotatingFilesystem} for an example
75+
*/
76+
auditLogger(message: object | unknown): void;
6777
};
6878

6979
export async function validateConfiguration(configuration: Configuration) {
@@ -142,4 +152,10 @@ export async function validateConfiguration(configuration: Configuration) {
142152
);
143153
}
144154
}
155+
if (typeof configuration.logger !== "function") {
156+
throw new Error(`configuration: logger must be a function`);
157+
}
158+
if (typeof configuration.auditLogger !== "function") {
159+
throw new Error(`configuration: auditLogger must be a function`);
160+
}
145161
}

relying-party/src/ConfigurationFacade.ts

Lines changed: 13 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1-
import * as fs from "fs";
2-
import { generateJWKS } from "./utils";
31
import { Configuration } from "./Configuration";
2+
import { auditLogRotatingFilesystem } from "./default-implementations/auditLogRotatingFilesystem";
3+
import { deriveFiscalNumberUserIdentifier } from "./default-implementations/deriveFiscalNumberUserIdentifier";
4+
import { loadOrCreateJWKSFromFilesystem } from "./default-implementations/loadOrCreateJWKSFromFilesystem";
5+
import { loadTrustMarksFromFilesystem } from "./default-implementations/loadTrustMarksFromFilesystem";
6+
import { logRotatingFilesystem } from "./default-implementations/logRotatingFilesystem";
47

58
/**
69
* This is a configuration facade to minimize setup effort.
@@ -19,8 +22,11 @@ export async function ConfigurationFacade({
1922
trust_anchors: Array<string>;
2023
identity_providers: Array<string>;
2124
}): Promise<Configuration> {
22-
const { public_jwks, private_jwks } = await loadOrCreateJWKS();
23-
const trust_marks = await loadTrustMarks();
25+
const { public_jwks, private_jwks } = await loadOrCreateJWKSFromFilesystem();
26+
const trust_marks = await loadTrustMarksFromFilesystem();
27+
const logger = logRotatingFilesystem;
28+
const auditLogger = auditLogRotatingFilesystem;
29+
const deriveUserIdentifier = deriveFiscalNumberUserIdentifier;
2430
return {
2531
client_id,
2632
client_name,
@@ -77,45 +83,8 @@ export async function ConfigurationFacade({
7783
private_jwks,
7884
trust_marks,
7985
redirect_uris: [client_id + "callback"],
80-
deriveUserIdentifier(claims) {
81-
const userIdentifierFields = ["https://attributes.spid.gov.it/fiscalNumber", "fiscalNumber"];
82-
if (!(typeof claims === "object" && claims !== null)) throw new Error();
83-
const claimsAsRecord = claims as Record<string, unknown>;
84-
for (const userIdentifierField of userIdentifierFields) {
85-
const value = claimsAsRecord[userIdentifierField];
86-
if (typeof value === "string") return value;
87-
}
88-
throw new Error();
89-
},
86+
deriveUserIdentifier,
87+
logger,
88+
auditLogger,
9089
};
9190
}
92-
93-
async function loadOrCreateJWKS() {
94-
const public_jwks_path = "./public.jwks.json";
95-
const private_jwks_path = "./private.jwks.json";
96-
if ((await fileExists(public_jwks_path)) && (await fileExists(private_jwks_path))) {
97-
const public_jwks = JSON.parse(await fs.promises.readFile(public_jwks_path, "utf8"));
98-
const private_jwks = JSON.parse(await fs.promises.readFile(private_jwks_path, "utf8"));
99-
return { public_jwks, private_jwks };
100-
} else {
101-
const { public_jwks, private_jwks } = await generateJWKS();
102-
await fs.promises.writeFile(public_jwks_path, JSON.stringify(public_jwks));
103-
await fs.promises.writeFile(private_jwks_path, JSON.stringify(private_jwks));
104-
return { public_jwks, private_jwks };
105-
}
106-
}
107-
108-
async function loadTrustMarks() {
109-
const trust_marks_path = "./trust_marks.json";
110-
if (await fileExists(trust_marks_path)) return JSON.parse(await fs.promises.readFile(trust_marks_path, "utf8"));
111-
else return [];
112-
}
113-
114-
async function fileExists(path: string) {
115-
try {
116-
await fs.promises.stat(path);
117-
return true;
118-
} catch (error) {
119-
return false;
120-
}
121-
}

0 commit comments

Comments
 (0)