-
Notifications
You must be signed in to change notification settings - Fork 22
/
Copy pathServer.js
239 lines (208 loc) · 7.94 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
const { UserAgentService } = require('./services/useragent');
const { WebServerService } = require('./services/webserver');
const { JwtService } = require('./services/jwt');
const { SamlBearerService } = require('./services/samlbearer');
const { UsernamePasswordService } = require('./services/usernamepassword');
const { DeviceService } = require('./services/device');
const { RefreshService } = require('./services/refresh');
const { SamlAssertService } = require('./services/samlassert');
// Load dependencies
const express = require('express'),
bodyParser = require('body-parser'),
morgan = require('morgan'),
app = express(),
https = require('https'),
fs = require('fs'),
rateLimit = require('express-rate-limit'),
data = require('./data/authFlows.json'),
flowsList = Object.values(data),
escape = require('escape-html');
// Set global variables, some loaded from environment variables (.env file)
const clientId = process.env.CLIENT_ID,
clientSecret = process.env.CLIENT_SECRET,
callbackURL = process.env.CALLBACK_URL,
baseURL = process.env.BASE_URL,
username = process.env.USERNAME,
limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Limit each IP to 100 requests per `window` (here, per 15 minutes)
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
legacyHeaders: false, // Disable the `X-RateLimit-*` headers
}),
flowClasses = {
'user-agent': UserAgentService,
'web-server': WebServerService,
'refresh-token': RefreshService,
'jwt-bearer': JwtService,
'saml-bearer': SamlBearerService,
'saml-assertion': SamlAssertService,
'username-password': UsernamePasswordService,
device: DeviceService,
};
// Global variable containing the instance
let authInstance, inputUsername, inputPassword;
// Set default view engine to ejs. This will be used when calling res.render().
app.set('view engine', 'ejs');
// Apply the rate limiting middleware to all requests
app.use(limiter);
// Let Express know where the client files are located
app.use(express.static(__dirname + '/client'));
// Setting up of app
app.use(morgan('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
// Set the port to use based on the environment variables
app.set('port', process.env.PORT);
/**
* Display the home page.
*/
app.route(/^\/(index.*)?$/).get(function (req, res) {
res.render('index', {
callbackURL: callbackURL,
baseURL: baseURL,
username: username,
clientId: clientId,
clientSecret: clientSecret,
data: flowsList,
});
});
app.get('/launch/:id', (req, res) => {
// Retrieve the data for the flow based on the id in the query parameters
const flowData = data[req.params.id];
// Launch specified flow with specified variant
let flowName = flowData.flow;
let variant = flowData.variant;
// If the authInstance variable is already set, retrieve the refreshToken and reset the activeCallback flag
let refreshToken;
let activeCallback;
if (authInstance) {
activeCallback = authInstance.isActiveCallback();
authInstance.setActiveCallback(false);
refreshToken = authInstance.refreshToken;
}
// Set up the authorization flow instance, except if there is an active callback.
if (!activeCallback) {
// Set up the auth flow instance
if (variant) {
authInstance = new flowClasses[flowName](variant);
} else if (inputUsername && inputPassword) {
authInstance = new flowClasses[flowName](inputUsername, inputPassword);
} else {
authInstance = new flowClasses[flowName]();
}
authInstance.refreshToken = refreshToken;
}
// Render the flow launch page
res.render('launchedFlow', {
data: flowsList,
authFlow: flowData,
});
});
app.get('/state', (req, res) => {
const step = authInstance.currentStep;
// For user-agent, we're parsing access token and id token on client side
// So we're passing these in when we're retrieving the state. Not ideal?
const newAccessToken = req.query.accessToken;
if (newAccessToken) {
authInstance.accessToken = newAccessToken;
}
const newIdToken = req.query.idToken;
if (newIdToken) {
authInstance.idToken = newIdToken;
}
const flowState = {
step: step,
baseURL: baseURL,
clientId: clientId,
clientSecret: clientSecret,
callbackURL: callbackURL,
authCode: authInstance.code,
accessToken: authInstance.accessToken,
refreshToken: authInstance.refreshToken,
idToken: authInstance.idToken,
request: authInstance.currentRequest,
response: authInstance.currentResponse,
};
res.send(flowState);
});
app.get('/execute-step', async (req, res) => {
let outcome;
if (req.query.direction === 'next') {
outcome = await authInstance.executeNextStep();
} else if (req.query.direction === 'previous') {
outcome = authInstance.returnToPreviousStep();
}
res.send(outcome);
});
/**
* Username Password oAuth Flow. Gets launched when navigating to '/username-password'.
* Sends username and password in the URL as free text to the token endpoint.
*/
app.post('/username-password', function (req, res) {
// Instantiate Username-Password service and generate post request
inputUsername = req.body.sfdcUsername;
inputPassword = req.body.sfdcPassword;
res.redirect('/launch/username-password');
});
/**
* This method is called every time we poll the token endpoint to see if the device
* was authorized. It only loads the page in case a response was received
*/
app.get('/devicePol', async (req, res) => {
authInstance.setActiveCallback(true);
await authInstance.pollTokenEndpoint();
res.redirect('/launch/device');
});
/**
* Handle OAuth callback from Salesforce and parse the result.
* Result is parsed in oauthcallback.ejs.
*/
app.get('/services/oauth2/success', function (req, res) {
let code = req.query.code;
let returnedState = escape(req.query.state);
let originalState = authInstance ? authInstance.state : undefined;
console.debug('Callback received with code %s and state %s', code, returnedState);
authInstance.setActiveCallback(true);
authInstance.currentResponse = req.originalUrl;
if (code) {
// If an authorization code is returned, check the state and continue web-server flow.
if (returnedState === originalState) {
authInstance.code = code;
res.redirect('/launch/web-server-client-secret');
} else {
res.status(500).end(
'Error occurred: ' +
'\nCross App / Site Request Forgery detected!' +
'\nReturned state: ' +
returnedState +
'\nOriginal state: ' +
originalState
);
}
} else {
// If no authorization code is returned, render oauthcallback.
// We need client-side Javascript to get to the fragment (after #) of the URL.
res.redirect('/launch/user-agent');
}
});
app.get('/devicecallback', (req, res) => {
res.render('deviceOAuth', {
verification_uri: req.query.verification_uri,
user_code: req.query.user_code,
data: flowsList,
});
});
/**
* Log message to indicate on which port the application is running.
*/
app.listen(app.get('port'), function () {
console.log('Express server listening on port ' + app.get('port'));
});
// Load files with private key and corresponding public certificate
const options = {
key: fs.readFileSync('./key.pem', 'utf8'),
cert: fs.readFileSync('./server.crt', 'utf8'),
};
// Create the server and log that it's up and running
https.createServer(options, app).listen(8081);
console.log('Server listening for HTTPS connections on port ', 8081);