Skip to content

Commit

Permalink
feat: add logging
Browse files Browse the repository at this point in the history
  • Loading branch information
freddi301 committed Mar 30, 2022
1 parent 8d5f659 commit f51ab4b
Show file tree
Hide file tree
Showing 23 changed files with 584 additions and 178 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,6 @@ dist

# TernJS port file
.tern-port

# SQLite local files
*.sqlite
2 changes: 1 addition & 1 deletion examples/express-react-relaying-party/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
23 changes: 16 additions & 7 deletions examples/express-react-relaying-party/backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
AgnosticRequest,
AgnosticResponse,
EndpointHandlers,
} from "@spid-cie-oidc-nodejs/relying-party";
} from "spid-cie-oidc";

main();
async function main() {
Expand All @@ -29,13 +29,19 @@ async function main() {
} = await EndpointHandlers(configuration);

function adaptRequest(req: Request): AgnosticRequest<any> {
return { query: req.query };
return {
url: `${req.protocol}://${req.get("host")}${req.originalUrl}`,
headers: req.headers as Record<string, string>,
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);
}
}
Expand All @@ -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);
});

Expand Down Expand Up @@ -76,15 +83,18 @@ 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
});
adaptReponse(response, res);
});

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);
});

Expand Down Expand Up @@ -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?
4 changes: 2 additions & 2 deletions relying-party/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
6 changes: 4 additions & 2 deletions relying-party/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand All @@ -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",
Expand Down
32 changes: 28 additions & 4 deletions relying-party/src/AccessTokenRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,21 +32,45 @@ 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: {
"content-type": "application/x-www-form-urlencoded",
},
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;
}
52 changes: 40 additions & 12 deletions relying-party/src/AuthenticationRequest.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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,
Expand Down
20 changes: 18 additions & 2 deletions relying-party/src/Configuration.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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`);
}
}
57 changes: 13 additions & 44 deletions relying-party/src/ConfigurationFacade.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -19,8 +22,11 @@ export async function ConfigurationFacade({
trust_anchors: Array<string>;
identity_providers: Array<string>;
}): Promise<Configuration> {
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,
Expand Down Expand Up @@ -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<string, unknown>;
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;
}
}
Loading

0 comments on commit f51ab4b

Please sign in to comment.