Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cdr arrangement jwt merge with latest #85

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"watch-templates": "node node_modules/chokidar-cli/index.js src/**/*template.hbs src/**/Dependencies.yml src/**/Dependencies.generator.js -c \"npm run build-templates\"",
"build-templates": "node ./src/Common/Connectivity/Dependencies.generator.js",
"test": "cross-env TEST_SUITE_MOCK_ONLY=1 TEST_SUITE_HEADLESS=1 LOG_LEVEL=silent LOG_SILENT=1 mocha \"dist/Tests/**/*spec.js\"",
"test-wip": "cross-env TEST_SUITE_MOCK_ONLY=1 TEST_SUITE_HEADLESS=1 LOG_LEVEL=silent LOG_SILENT=1 mocha --grep wip \"dist/Tests/**/*spec.js\"",
"test-live": "cross-env TEST_SUITE_HEADLESS=1 LOG_LEVEL=silent LOG_SILENT=1 mocha --grep \"Live environments\" \"dist/Tests/**/*spec.js\"",
"test-coverage": "cross-env TEST_SUITE_MOCK_ONLY=1 TEST_SUITE_HEADLESS=1 LOG_LEVEL=silent LOG_SILENT=1 nyc --reporter text --reporter html --reporter cobertura npm run test",
"copy-proj": "npx rimraf examples/deployment/docker/.work && npx cpx \"src/**\" \"examples/deployment/docker/.work/src\" && npx cpx \"{package,package-lock,tsconfig}.json\" \"examples/deployment/docker/.work\""
Expand Down
102 changes: 77 additions & 25 deletions src/AdrServer/Server/Handlers/DeleteArrangement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,73 +2,125 @@ import _ from "lodash";
import express, { urlencoded } from "express";
import { NextFunction } from "connect";

import { validationResult, matchedData, body} from 'express-validator'
import { validationResult, matchedData, body } from 'express-validator'
import { injectable, inject } from "tsyringe";
import winston from "winston";
import { ConsentRequestLogManager, ConsentRequestLog } from "../../../Common/Entities/ConsentRequestLog";
import moment from "moment";
import { GatewayRequest } from "../../../Common/Server/Types";
import { DefaultConnector } from "../../../Common/Connectivity/Connector.generated";
import { JWKS, JWT, JWS } from "jose";
import { JoseBindingConfig } from "../../../Common/Server/Config";
import urljoin from "url-join";

interface ClientJWTPayload {
iss: string;
sub: string;
aud: string;
jti?: string;
exp?: string;
iat?: string;
nbf?: string;
}

@injectable()
export class DeleteArrangementMiddleware {
constructor(
@inject("Logger") private logger:winston.Logger,
private consentManager:ConsentRequestLogManager,
){}

@inject("Logger") private logger: winston.Logger,
private consentManager: ConsentRequestLogManager,
private connector: DefaultConnector,
@inject("JoseBindingConfig") private configFn: () => Promise<JoseBindingConfig>
) { }

handler = () => {
let validationErrorMiddleware = (req:express.Request,res:express.Response,next: NextFunction) => {
let validationErrorMiddleware = (req: express.Request, res: express.Response, next: NextFunction) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ error:"invalid_request", errors: errors.array() });
return res.status(400).json({ error: "invalid_request", errors: errors.array() });
}
next();
}

let RevocationResponder = async (req:express.Request,res:express.Response,next: NextFunction) => {
let RevocationResponder = async (req: express.Request, res: express.Response, next: NextFunction) => {

let m:{
cdr_arrangement_id: string
let m: {
cdr_arrangement_jwt: string
} = <any>matchedData(req);

console.log("Delete arrangement call", m.cdr_arrangement_jwt)

// verify the cdr_arrangement_jwt
let authHeader = req.headers['authorization']
if (typeof authHeader == 'undefined') throw new Error("Authorization header is not present");
if (!authHeader.startsWith("Bearer ")) throw new Error("Bearer token expected but not supplied");

let bearerTokenJwt: string = authHeader.substr("Bearer ".length);
let authPayload = <ClientJWTPayload>JWT.decode(bearerTokenJwt);
let assumedClientId = authPayload?.sub
if (typeof assumedClientId !== 'string') throw new Error("JWT sub claim is not a string");
let jwks = await this.connector.DataHolderRevocationJwks(assumedClientId).GetWithHealing({
validator: (jwks) => {
JWS.verify(bearerTokenJwt, jwks);
return true;
}
});

let verified = <object | undefined>undefined;

// verify cdr_arrangement_jwt
verified = JWT.verify(m.cdr_arrangement_jwt, jwks, {
complete: false, // set as false since we may not get all the jwt claims from this
algorithms: ["PS256", "ES256"]
});

// further checks aside from jose processing
if (typeof verified == 'undefined') throw 'Verified JWT payload expected, but is undefined'

// decode the cdr_arrangement_id from the cdr_arrangement_jwt
let bodyPayload: {
cdr_arrangement_id?: string,
} = JWT.decode(m.cdr_arrangement_jwt)

// this is end

let verifiedClientId = (req as GatewayRequest)?.gatewayContext?.verifiedBearerJwtClientId

this.logger.debug({
message: "Received arrangement revocation request",
meta: {token: m.cdr_arrangement_id, verifiedClientId},
meta: { token: bodyPayload.cdr_arrangement_id, verifiedClientId },
date: moment().toISOString()
});

try {

let consentManager = this.consentManager;
let consents:ConsentRequestLog[] = await consentManager.GetConsentsByDeleteArrangementParams(m.cdr_arrangement_id,verifiedClientId);
let consents: ConsentRequestLog[] = await consentManager.GetConsentsByDeleteArrangementParams(bodyPayload.cdr_arrangement_id, verifiedClientId);

// only revoke current consents
consents = consents.filter(c => c.IsCurrent());

if (consents.length < 1) {
return res.sendStatus(422);
}

for (let consent of consents) {
await consentManager.RevokeConsent(consent,"DataHolder");
await consentManager.RevokeConsent(consent, "DataHolder");
this.logger.info({
message: "Revoked consent",
correlationId:(<any>req).correlationId,
meta: {cdr_arrangement_id: m.cdr_arrangement_id},
correlationId: (<any>req).correlationId,
meta: { cdr_arrangement_id: bodyPayload.cdr_arrangement_id },
date: moment().toISOString()
});

}

return res.sendStatus(204);

} catch (e) {
this.logger.error({
message: "Failed token revocation request",
correlationId:(<any>req).correlationId,
meta: {token: m.cdr_arrangement_id},
correlationId: (<any>req).correlationId,
meta: { token: m.cdr_arrangement_jwt },
error: e,
date: moment().toISOString()
});
Expand All @@ -78,10 +130,10 @@ export class DeleteArrangementMiddleware {
};

return [
urlencoded({extended:true}),
body('cdr_arrangement_id').isString(),
urlencoded({ extended: true }),
body('cdr_arrangement_jwt').isString(),
validationErrorMiddleware,
RevocationResponder
];
}
}
}
10 changes: 10 additions & 0 deletions src/Common/Connectivity/Assertions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,16 @@ export const CreateAssertion = (client_id:string,endpoint:string,keystore:JWKS.K

let jwk = keystore.get({use:'sig',alg:"PS256"});

let assertion = JWT.sign(claims,jwk);
return assertion;
}

export const CreateCDRArrangementJWTAssertion = (cdr_arrangement_id: string, keystore:JWKS.KeyStore) => {
let claims = {
cdr_arrangement_id: cdr_arrangement_id
}
let jwk = keystore.get({use:'sig',alg:"PS256"});

let assertion = JWT.sign(claims,jwk);
return assertion;
}
6 changes: 3 additions & 3 deletions src/Common/Connectivity/Evaluators/PropagateRevokedConsent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as Types from "../Types"
import { ClientCertificateInjector } from "../../Services/ClientCertificateInjection";
import { AxiosRequestConfig } from "axios";
import qs from "qs";
import { CreateAssertion } from "../../Connectivity/Assertions";
import { CreateAssertion, CreateCDRArrangementJWTAssertion } from "../../Connectivity/Assertions";
import moment from "moment";
import { axios } from "../../Axios/axios";
import { ConsentRequestLogManager } from "../../Entities/ConsentRequestLog";
Expand Down Expand Up @@ -30,13 +30,13 @@ export const PropagateRevokedConsent = async (logger: winston.Logger, cert: Clie
url,
responseType: "json",
data: qs.stringify({
"cdr_arrangement_id": $.Consent.arrangementId,
// "cdr_arrangement_id": $.Consent.arrangementId,
"cdr_arrangement_jwt": CreateCDRArrangementJWTAssertion($.Consent.arrangementId, $.DataRecipientJwks),
"client_id": $.CheckAndUpdateClientRegistration.clientId,
"client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
"client_assertion": CreateAssertion($.CheckAndUpdateClientRegistration.clientId, url, $.DataRecipientJwks),
})
}

cert.inject(options,$.Consent.softwareProductId);
let response = await axios.request(options);

Expand Down
72 changes: 41 additions & 31 deletions src/MockServices/DhServer/Server/Handlers/DeleteArrangement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import _ from "lodash";
import express from "express";
import { NextFunction } from "connect";

import { validationResult, matchedData, body, param} from 'express-validator'
import { validationResult, matchedData, body, param } from 'express-validator'
import { inject, injectable } from "tsyringe";
import winston from "winston";
import { JWT, JWKS } from "jose";
Expand All @@ -15,79 +15,89 @@ import { ClientCertificateInjector } from "../../../../Common/Services/ClientCer
@injectable()
export class DeleteArrangementMiddleware {
constructor(
@inject("Logger") private logger:winston.Logger,
@inject("Logger") private logger: winston.Logger,
private clientRegistrationManager: ClientRegistrationManager,
@inject("ClientCertificateInjector") private mtls: ClientCertificateInjector,
private consentManager:ConsentManager,
){}
private consentManager: ConsentManager,
) { }


handler = () => {
let validationErrorMiddleware = (req:express.Request,res:express.Response,next: NextFunction) => {
let validationErrorMiddleware = (req: express.Request, res: express.Response, next: NextFunction) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ error: "invalid_request" });
return res.status(400).json({ error: "invalid_request" });
}
next();
}
let Responder = async (req:express.Request,res:express.Response,next: NextFunction) => {
res.setHeader('Cache-Control','no-store')
res.setHeader('Pragma','no-cache')
let params:{

let Responder = async (req: express.Request, res: express.Response, next: NextFunction) => {

res.setHeader('Cache-Control', 'no-store')
res.setHeader('Pragma', 'no-cache')

let params: {
client_assertion: string
cdr_arrangement_id: string
cdr_arrangement_jwt: string
client_id: string
} = <any>matchedData(req)

// TODO move this client credntial check to an auth middleware
let client = await this.clientRegistrationManager.GetRegistration(params.client_id);

if (typeof client == 'undefined') return res.status(401).json({error:"invalid_client"});
if (typeof client == 'undefined') return res.status(401).json({ error: "invalid_client" });

// GET the JWKS for signing

let client_jwks = JWKS.asKeyStore(await (await axios.get(client.jwks_uri, this.mtls.injectCa({responseType:"json"}))).data)
let client_jwks = JWKS.asKeyStore(await (await axios.get(client.jwks_uri, this.mtls.injectCa({ responseType: "json" }))).data)

// verify the JWT
let payload:any;
let payload: any;
try {
payload = JWT.verify(params.client_assertion, client_jwks, { algorithms: ["PS256"] })
for (let key of ['aud', 'jti', 'exp', 'iss', 'sub'])
if (typeof payload[key] === 'undefined') {
throw `key ${key} is missing from JWT`
}
} catch (e) {
return res.status(401).json({ error: "invalid_client" })
}
this.logger.info("Revoked arrangement cdr_arrangement_jwt: " + params.cdr_arrangement_jwt);
let cdr_arrangement_object: any;
try {
payload = JWT.verify(params.client_assertion,client_jwks,{algorithms:["PS256"]})
for (let key of ['aud','jti','exp','iss','sub'])
if (typeof payload[key] === 'undefined') {
throw `key ${key} is missing from JWT`
cdr_arrangement_object = JWT.verify(params.cdr_arrangement_jwt, client_jwks, { algorithms: ["PS256"] })
if (typeof cdr_arrangement_object.cdr_arrangement_id === 'undefined') {
throw `cdr_arrangement_id is missing from cdr_arrangement_jwt`
}
} catch (e) {
return res.status(401).json({error:"invalid_client"})
return res.status(401).json({ error: "invalid cdr_arrangement_jwt" })
}

let dataRecipientId = <string>req.body.client_id;

if (typeof params.cdr_arrangement_id !== 'string') {
if (typeof cdr_arrangement_object.cdr_arrangement_id !== 'string') {
res.statusCode = 400;
res.json({ error: "invalid_request" })
return;
}

await this.consentManager.revokeArrangement(params.cdr_arrangement_id, dataRecipientId);
await this.consentManager.revokeArrangement(cdr_arrangement_object.cdr_arrangement_id, dataRecipientId);

this.logger.info("Revoked arrangement: " + params.cdr_arrangement_id);
this.logger.info("Revoked arrangement: " + cdr_arrangement_object.cdr_arrangement_id);

res.sendStatus(204);

};

// decide whether to validate based on body or query parameters
// TODO add client authorization
return _.concat([
bodyParser.urlencoded({extended:true}),
body('cdr_arrangement_id').isString(),
bodyParser.urlencoded({ extended: true }),
body('cdr_arrangement_jwt').isString(),
body("client_id").isString(),
body('client_assertion_type').isString().equals("urn:ietf:params:oauth:client-assertion-type:jwt-bearer").withMessage("invalid client_assertion_type"),
body("client_assertion").isJWT()
],[
body("client_assertion").isJWT()
], [
<any>validationErrorMiddleware,
Responder
])
Expand Down
15 changes: 9 additions & 6 deletions src/Tests/EndToEnd/E2E-UAT-Scenarios.SecurityProfile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export const Tests = ((env:E2ETestEnvironment) => {
const CreateAssertion = async (...args:any[]) => (await GenerateTestData(env)).CreateAssertion.apply(undefined,<any>args)
const CreateDhBearerAuthJwt = async (...args:any[]) => (await GenerateTestData(env)).CreateDhBearerAuthJwt.apply(undefined,<any>args)
const CreateAssertionWithoutKey = async (...args:any[]) => (await GenerateTestData(env)).CreateAssertionWithoutKey.apply(undefined,<any>args)

const CreateCDRArrangementJWTAssertion = async (...args:any[]) => (await GenerateTestData(env)).CreateDhCdrArrangementIdJwt.apply(undefined,<any>args)


describe('Security Profile', async () => {
Expand Down Expand Up @@ -307,7 +307,7 @@ export const Tests = ((env:E2ETestEnvironment) => {
expect(introspection1Result.body.active).to.be.true

let revocationResult:HttpLogEntry = (await ctx.GetResult(SetValue,"Revocation")).value
expect(revocationResult.config.data).to.match(/cdr_arrangement_id=/);
expect(revocationResult.config.data).to.match(/cdr_arrangement_jwt=/); // change from cdr_arrangment_id to cdr_arrangement_jwt
expect(revocationResult.response.status).to.equal(204);

let introspection2Result = await ctx.GetResult(DoRequest,"Introspection2")
Expand Down Expand Up @@ -690,7 +690,7 @@ export const Tests = ((env:E2ETestEnvironment) => {

},600)

Scenario($ => it.apply(this,$('Revocation (cdr_arrangement_id)')), undefined, 'Data recipient honours valid revocation request')
Scenario($ => it.apply(this,$('wip Revocation (cdr_arrangement_id)')), undefined, 'Data recipient honours valid revocation request')
.Given('New Authorization')
.Precondition("DH private JWKS available", ctx => {
if (!ctx.environment.Config.SystemUnderTest.DhRevokePrivateJwks) {
Expand Down Expand Up @@ -736,12 +736,14 @@ export const Tests = ((env:E2ETestEnvironment) => {

const arrangementId = (await ctx.GetResult(NewGatewayConsent,"Consent2")).consent!.arrangementId
let url = urljoin(`https://localhost:${ctx.environment.TestServices.httpsProxy.adrServer.port}`,"arrangements/revoke");


// TODO: need to change cdr_arrangement_id to cdr_arrangement_jwt
let options = DoRequest.Options({
method: "POST",
url,
data: qs.stringify({
cdr_arrangement_id: arrangementId
cdr_arrangement_jwt: await CreateCDRArrangementJWTAssertion(arrangementId)
// cdr_arrangement_id: arrangementId
}),
headers: {
"Authorization": "Bearer "+await CreateDhBearerAuthJwt(url)
Expand Down Expand Up @@ -782,7 +784,8 @@ export const Tests = ((env:E2ETestEnvironment) => {
let options = DoRequest.Options({
method: "POST",
data: qs.stringify({
cdr_arrangement_id: arrangementId
cdr_arrangement_jwt: await CreateCDRArrangementJWTAssertion(arrangementId)
// cdr_arrangement_id: arrangementId
}),
url,
headers: {
Expand Down
Loading