Skip to content

Commit

Permalink
feat: add userinfo route (#80)
Browse files Browse the repository at this point in the history
* Factor cookie extraction out for use elsewhere

* Remove dead code

* Remove dead code

* Refactor constants onto app context

* Add userinfo route

* Improved logging of health check failure

* Bugfix

* Tests. Version.

* Route handler test

* Use jsonwebtoken instead of jwt-decode

* Return userinfo from login route

* Set Path correctly when clearing token cookie on logout
  • Loading branch information
partiallyordered authored Sep 8, 2021
1 parent 9ddc3af commit 45afa83
Show file tree
Hide file tree
Showing 11 changed files with 10,097 additions and 512 deletions.
18 changes: 10 additions & 8 deletions src/handlers/login.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
const fetch = require('node-fetch');
const https = require('https');
const qs = require('querystring');
const { buildUserInfoResponse } = require('../lib/handlerHelpers');

// Create an https agent for use with self-signed certificates
// TODO: do we need this? It's used when contacting wso2. Does wso2 have a self-signed cert?
const selfSignedAgent = new https.Agent({ rejectUnauthorized: false });

const handler = (router, routesContext) => {
// TODO: set the Max-Age directive corresponding to the token expiry time.
const cookieDirectives = (token, insecure) => (
const cookieDirectives = (cookieName, token, insecure) => (
insecure
? `${routesContext.constants.TOKEN_COOKIE_NAME}=${token}; Path=/`
: `${routesContext.constants.TOKEN_COOKIE_NAME}=${token}; HttpOnly; SameSite=strict; Secure; Path=/`
? `${cookieName}=${token}; Path=/`
: `${cookieName}=${token}; HttpOnly; SameSite=strict; Secure; Path=/`
);

router.post('/login', async (ctx, next) => {
Expand Down Expand Up @@ -39,11 +39,13 @@ const handler = (router, routesContext) => {
return;
}

ctx.response.body = {
expiresIn: oauth2Token.expires_in,
};
ctx.response.body = buildUserInfoResponse(oauth2Token.access_token);
ctx.response.set({
'Set-Cookie': cookieDirectives(oauth2Token.access_token, routesContext.config.insecureCookie),
'Set-Cookie': cookieDirectives(
ctx.constants.TOKEN_COOKIE_NAME,
oauth2Token.access_token,
routesContext.config.insecureCookie,
),
});
ctx.response.status = 200;

Expand Down
2 changes: 1 addition & 1 deletion src/handlers/logout.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const handler = (router, routesContext) => {
};
await fetch(routesContext.config.auth.revokeEndpoint, opts);
ctx.response.set({
'Set-Cookie': `${routesContext.constants.TOKEN_COOKIE_NAME}=deleted; expires=Thu, 01 Jan 1970 00:00:00 GMT;HttpOnly; SameSite=strict${routesContext.config.insecureCookie ? '' : '; Secure'}`,
'Set-Cookie': `${ctx.constants.TOKEN_COOKIE_NAME}=deleted; Path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT;HttpOnly; SameSite=strict${routesContext.config.insecureCookie ? '' : '; Secure'}`,
});
ctx.response.body = {
status: 'Ok',
Expand Down
15 changes: 15 additions & 0 deletions src/handlers/userinfo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
const { getTokenCookieFromRequest, buildUserInfoResponse } = require('../lib/handlerHelpers');

const handler = (router) => {
router.get('/userinfo', async (ctx, next) => {
// Our request has already passed token validation
const tokenEnc = getTokenCookieFromRequest(ctx);

ctx.response.body = buildUserInfoResponse(tokenEnc);
ctx.response.status = 200;

await next();
});
};

module.exports = handler;
167 changes: 27 additions & 140 deletions src/lib/handlerHelpers.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,28 @@
const Big = require('big.js');
const casaLib = require('@mojaloop/finance-portal-lib');

const { participantFundsOutPrepareReserve, participantFundsInReserve } = casaLib.admin.api;
const jwt = require('jsonwebtoken');

// The cookie _should_ look like:
// mojaloop-portal-token=abcde
// But when doing local development, the cookie may look like:
// some-rubbish=whatever; mojaloop-portal-token=abcde; other-rubbish=defgh
// because of other cookies set on the host. So we take some more care extracting it here.
const getTokenCookieFromRequest = (ctx) => ctx.request
// get the cookie header string, it'll look like
// some-rubbish=whatever; token=abcde; other-crap=defgh
.get('Cookie')
// Split it so we have some key-value pairs that look like
// [['some-rubbish', 'whatever'], ['token', 'abcde'], ['other-rubbish', 'defgh']]
?.split(';')
?.map((cookie) => cookie.trim().split('='))
// Find the token cookie and get its value
// We assume there's only one instance of our cookie
?.find(([name]) => name === ctx.constants.TOKEN_COOKIE_NAME)?.[1];

// This function is shared to ensure /login and /userinfo return the same result
const buildUserInfoResponse = (encodedJwtToken) => {
const token = jwt.decode(encodedJwtToken);
const username = token.sub.split('@')[0];
return { username };
};

const getSettlementWindows = async (routesContext, fromDateTime, toDateTime,
settlementWindowId) => {
Expand Down Expand Up @@ -57,142 +78,8 @@ const getSettlementWindows = async (routesContext, fromDateTime, toDateTime,
return settlementWindows;
};

const filterParticipants = (participants, filter) => participants
.filter((participant) => participant.accounts
.findIndex((account) => filter(account.netSettlementAmount.amount)) !== -1)
.map((participant) => ({
...participant,
accounts: participant.accounts
.filter((account) => filter(account.netSettlementAmount.amount)),
}));

// Payers' settlement amounts will be positive while payees' will be negative
const getPayers = (participants) => filterParticipants(participants, (x) => x > 0);
const getPayees = (participants) => filterParticipants(participants, (x) => x < 0);

const newParticipantsAccountStateAndReason = (participants, reason, state) => participants
.map((participant) => ({
...participant,
accounts: participant.accounts.map((account) => ({ id: account.id, reason, state })),
}));

const getParticipantName = (dfsps, participants, participantId) => {
const accountIds = participants.find((participant) => String(participant.id) === participantId)
.accounts.map((account) => String(account.id));
const participant = dfsps.find((dfsp) => {
const dfspAccountIds = dfsp.accounts.map((account) => String(account.id));
const accountPresent = dfspAccountIds
.some((accountId) => accountIds.includes(accountId));
return accountPresent;
});
try {
const { name } = participant;
return name;
} catch (error) {
throw new Error('Could not find participant\'s name via its account from list of DFSP\'s');
}
};

const getAllParticipantNames = (dfsps, participants, paymentMatrix) => paymentMatrix
.map((payment) => {
const [, participantId] = payment;
const participantName = getParticipantName(dfsps, participants, participantId);
return participantName;
});

const getSettlementAccountId = (accounts, currency) => accounts
.find((account) => account.isActive === 1
&& account.ledgerAccountType === 'SETTLEMENT'
&& account.currency === currency)
.id;

const getParticipantAccounts = (dfsps, participantName) => {
try {
const participantDfsp = dfsps.find((dfsp) => {
const { name } = dfsp;
const equal = name === participantName;
return equal;
});
const { accounts } = participantDfsp;
return accounts;
} catch (error) {
throw new Error('Could not find participant\'s accounts via its name from list of DFSP\'s');
}
};

const processPaymentAndReturnFailedPayment = async (payment, dfsps, participants,
centralLedgerEndpoint, log) => {
try {
const [currency, participantId, amount] = payment;
const participantName = getParticipantName(dfsps, participants, participantId);
const accounts = getParticipantAccounts(dfsps, participantName);
const accountId = getSettlementAccountId(accounts, currency);
if (amount < 0) { // Funds Out
await participantFundsOutPrepareReserve(
centralLedgerEndpoint,
participantName,
accountId,
Math.abs(amount), // we need to send positive amount
currency,
'Admin portal funds out request',
log,
);
} else { // Funds In
await participantFundsInReserve(
centralLedgerEndpoint,
participantName,
accountId,
Math.abs(amount),
'Admin portal funds in request',
currency,
log,
);
}
} catch (error) {
log(`Error while processing the funds: ${error}`);
return payment;
}
return null;
};

const processPaymentsMatrixAndGetFailedPayments = async (paymentMatrix, dfsps, participants,
centralLedgerEndpoint, log) => {
const paymentProcessingResults = await Promise.all(paymentMatrix.map(
async (unprocessedPayment) => processPaymentAndReturnFailedPayment(
unprocessedPayment, dfsps, participants, centralLedgerEndpoint, log,
),
));
const failedPayments = paymentProcessingResults.filter((result) => result !== null);
return failedPayments;
};

const bigifyPaymentMatrix = (paymentMatrix, createBigNum = Big) => paymentMatrix
.map(([currency, participantId, amount]) => ([currency, participantId, createBigNum(amount)]));

const segmentParticipants = (participants) => {
const payers = getPayers(participants);
const payees = getPayees(participants);

const payerParticipants = newParticipantsAccountStateAndReason(payers,
'Payer: SETTLED, settlement: SETTLED', 'SETTLED');
const payeeParticipants = newParticipantsAccountStateAndReason(payees,
'Payee: SETTLED, settlement: SETTLED', 'SETTLED');
const allParticipants = newParticipantsAccountStateAndReason(participants,
'All Participants: SETTLED, settlement: SETTLED', 'SETTLED');

return { payerParticipants, payeeParticipants, allParticipants };
};

module.exports = {
buildUserInfoResponse,
getSettlementWindows,
getSettlementAccountId,
bigifyPaymentMatrix,
getPayees,
getPayers,
getParticipantName,
newParticipantsAccountStateAndReason,
getAllParticipantNames,
segmentParticipants,
getParticipantAccounts,
processPaymentsMatrixAndGetFailedPayments,
getTokenCookieFromRequest,
};
Loading

0 comments on commit 45afa83

Please sign in to comment.