-
Notifications
You must be signed in to change notification settings - Fork 3
/
server.js
326 lines (279 loc) · 11.1 KB
/
server.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
/*
* Copyright 2019 Jack Henry & Associates, Inc.®
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// Note:
// If you're learning how to build a *plugin*, you should use this other example project -
// https://github.com/Banno/simple-plugin-example
'use strict';
const fs = require('fs');
const fetch = require('node-fetch');
const { Strategy, Issuer, custom } = require('openid-client');
const passport = require('passport');
const express = require('express');
const session = require('express-session');
const config = require('./config');
console.log('Note: This is a local development server, it is meant as a demonstration of how to use the Banno OpenID Connect API. It is not meant to be used in production.');
console.log('API_ENVIRONMENT: ' + config.consumerApi.environment);
(async () => {
// Configure the OpenID Connect client based on the issuer.
const issuer = await Issuer.discover(config.issuer['garden']);
const client = new issuer.Client(config.client['garden']);
client[custom.clock_tolerance] = 300; // to allow a 5 minute clock skew for verification
// This example project doesn't include any storage mechanism(e.g. a database) for access tokens.
// Therefore, we use this as our 'storage' for the purposes of this example.
// This method is NOT recommended for use in production systems.
let accessToken;
const claims = {
given_name: null,
family_name: null,
name: null,
address: null,
phone: null,
email: null,
'https://api.banno.com/consumer/claim/institution_id': null
};
// Configure the Passport strategy for OpenID Connect.
const passportStrategy = new Strategy({
client: client,
params: {
redirect_uri: config.client['garden'].redirect_uris[0],
// These are the OpenID Connect scopes that you'll need to:
// - receive a Refresh Token
// - get read-only access to Accounts data
// - get read-only access to Transactions data
//
// For general information on scopes and claims, see https://jackhenry.dev/open-api-docs/authentication-framework/overview/openidconnectoauth/.
//
// For specific information on scopes for API endpoints, see the definitions in https://jackhenry.dev/open-api-docs/consumer-api/api-reference/.
// Every API endpoint documented in our API Reference includes information on the scope necessary to use that endpoint.
scope: 'openid https://api.banno.com/consumer/auth/offline_access https://api.banno.com/consumer/auth/accounts.readonly https://api.banno.com/consumer/auth/transactions.detail.readonly',
claims: JSON.stringify({
// Authenticated information about the user can be returned in these ways:
// - as Claims in the Identity Token,
// - as Claims returned from the UserInfo Endpoint,
// - as Claims in both the Identity Token and from the UserInfo Endpoint.
//
// See https://openid.net/specs/openid-connect-core-1_0.html#ClaimsParameter
id_token: claims,
userinfo: claims
})
},
usePKCE: true
}, (tokenSet, done) => {
console.log(tokenSet);
accessToken = tokenSet.access_token;
return done(null, tokenSet.claims());
});
const port = process.env.PORT || 8080
const app = express();
app.use(session({
secret: 'foo',
resave: false,
saveUninitialized: true,
}));
app.use(passport.initialize());
app.use(passport.session());
passport.use('openidconnect', passportStrategy);
passport.serializeUser((user, done) => done(null, user));
passport.deserializeUser((user, done) => done(null, user));
app.get('/', (req, res, next) => {
res.redirect('/hello');
});
// This routing path handles the start of an authentication request.
// This is the path used in '/login.html' when you click the 'Sign in with Banno' button.
app.get('/auth', (req, res, next) => {
const options = {
// Random string for state
state: Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15)
};
req.session.oAuthState = req.session.oAuthState || {};
req.session.oAuthState[options.state] = {};
// If we have a deep link path query parameter, save it in a state parameter
// so that we can redirect to the correct page when the OAuth flow completes
// See https://auth0.com/docs/protocols/oauth2/redirect-users
if (req.query.returnPath && req.query.returnPath[0] === '/') {
req.session.oAuthState[options.state].returnPath = req.query.returnPath;
}
return passport.authenticate('openidconnect', options)(req, res, next);
}
);
// This routing path handles the authentication callback.
// This path (including the host information) must be configured in Banno SSO settings.
app.get('/auth/cb', (req, res, next) => {
passport.authenticate('openidconnect', (err, user, info) => {
console.log(err, user, info);
if (err || !user) {
return res.redirect('/login.html');
}
const options = {
keepSessionInfo: true
}
req.logIn(user, options, (err) => {
if (err) {
return next(err);
}
let nextPath = '/me';
// If a state parameter is present and has a matching local state, lookup the value
if (req.query.state) {
if (req.session.oAuthState && req.session.oAuthState[req.query.state]) {
if (req.session.oAuthState[req.query.state].returnPath) {
nextPath = req.session.oAuthState[req.query.state].returnPath;
}
delete req.session.oAuthState[req.query.state];
} else {
console.error('State parameter not found in store');
return res.redirect('/login.html');
}
}
return res.redirect(nextPath);
});
})(req, res, next)
});
// This routing path shows the OpenID Connect claims for the authenticated user.
// This path is where you'll be redirected once you sign in.
app.get('/me', (req, res) => {
if (!req.isAuthenticated()) {
res.redirect('/login.html?returnPath=/me');
return;
}
res.set('Content-Type', 'application/json').send(JSON.stringify(req.session.passport.user, undefined, 2));
});
app.get('/hello', (req, res) => {
if (!req.isAuthenticated()) {
res.redirect('/login.html?returnPath=/hello');
return;
}
res.set('Content-Type', 'text/plain').send(`Hello ${req.session.passport.user.name}`);
});
// This routing path shows the Accounts and Transactions for the authenticated user.
app.get('/accountsAndTransactions', (req, res) => {
if (!req.isAuthenticated()) {
res.redirect('/login.html?returnPath=/accountsAndTransactions');
return;
}
const userId = req.session.passport.user.sub;
getAccountsAndTransactions(userId, res);
});
app.use(express.static('public'));
// Previous versions of this demo used provided certs to run a secure server,
// this is no longer neccesary for localhost
app.listen(port, () => console.log(`Server listening on http://localhost:${port}`))
async function getAccountsAndTransactions(userId, res) {
// Set up
const consumerApiPath = `${config.consumerApi.environment}${config.consumerApi.usersBase}`;
let output = '';
// PUT Fetch
const taskId = await putFetch(consumerApiPath, userId, accessToken);
// GET Tasks
await getTasksUntilTaskEndedEventIsReceived(consumerApiPath, userId, taskId, accessToken);
// GET Accounts
const accounts = await getAccounts(consumerApiPath, userId, accessToken);
for (const account of accounts) {
const accountId = account.id;
const accountBalance = account.balance;
const accountType = account.accountType;
const accountSubtype = account.accountSubType;
const accountRoutingNumber = account.routingNumber;
output += `
Account ID: ${accountId}
Balance: ${accountBalance}
Type: ${accountType}
Subtype: ${accountSubtype}
Routing Number: ${accountRoutingNumber}
`;
// GET Transactions
const transactions = await getTransactions(consumerApiPath, userId, accountId, accessToken);
if (transactions != null){
transactions.forEach(transaction => {
const transactionId = transaction.id;
const transactionAccountId = transaction.accountId;
const transactionAmount = transaction.amount;
const transactionMemo = transaction.memo;
output += `
Transaction ID: ${transactionId}
Account ID: ${transactionAccountId}
Amount: ${transactionAmount}
Memo: ${transactionMemo}
`;
});
} else {
output += `
No transactions for this account.
`
}
}
res.set('Content-Type', 'text/plain').send(output);
}
async function getTasksUntilTaskEndedEventIsReceived(consumerApiPath, userId, taskId, accessToken) {
let taskEndedEventReceived = false;
while (taskEndedEventReceived != true) {
const events = await getTasks(consumerApiPath, userId, taskId, accessToken);
events.forEach(event => {
const eventType = event.type;
if (eventType == 'TaskEnded') {
taskEndedEventReceived = true;
}
});
if (!taskEndedEventReceived) {
// We should wait a while while the aggregation work is performed on the server,
// then we can check again.
sleep(3000);
}
}
}
async function getTransactions(consumerApiPath, userId, accountId, accessToken) {
const transactionsApiResponse = await fetch(`${consumerApiPath}${userId}/accounts/${accountId}/transactions`, {
method: 'get',
headers: { 'Authorization': 'Bearer ' + accessToken }
});
const transactionsApiJson = await transactionsApiResponse.json();
const transactions = transactionsApiJson.transactions;
return transactions;
}
async function getAccounts(consumerApiPath, userId, accessToken) {
const accountsApiResponse = await fetch(`${consumerApiPath}${userId}/accounts`, {
method: 'get',
headers: { 'Authorization': 'Bearer ' + accessToken }
});
const accountsApiJson = await accountsApiResponse.json();
const accounts = accountsApiJson.accounts;
return accounts;
}
async function getTasks(consumerApiPath, userId, taskId, accessToken) {
const tasksApiResponse = await fetch(`${consumerApiPath}${userId}/tasks/${taskId}`, {
method: 'get',
headers: { 'Authorization': 'Bearer ' + accessToken }
});
const tasksApiJson = await tasksApiResponse.json();
const events = tasksApiJson.events;
return events;
}
async function putFetch(consumerApiPath, userId, accessToken) {
const fetchApiResponse = await fetch(`${consumerApiPath}${userId}/fetch`, {
method: 'put',
headers: { 'Authorization': 'Bearer ' + accessToken }
});
const fetchApiJson = await fetchApiResponse.json();
const taskId = fetchApiJson.taskId;
return taskId;
}
function sleep(milliseconds) {
const date = Date.now();
let currentDate = null;
do {
currentDate = Date.now();
} while (currentDate - date < milliseconds);
}
})();