1 /*jshint node:true, indent:2, curly:false, eqeqeq:true, immed:true, latedef:true, newcap:true, noarg:true,
2 regexp:true, undef:true, strict:true, trailing:true, white:true, expr:true */
3 /*global X:true, SYS:true, _:true, console:true*/
8 var auth = require('../routes/auth'),
9 oauth2orize = require('oauth2orize'),
10 jwtBearer = require('oauth2orize-jwt-bearer').Exchange,
11 passport = require('passport'),
12 login = require('connect-ensure-login'),
15 utils = require('./utils'),
16 privateSalt = X.fs.readFileSync(X.options.datasource.saltFile).toString();
18 // create OAuth 2.0 server
19 var server = oauth2orize.createServer();
21 // Register serialialization and deserialization functions.
23 // When a client redirects a user to user authorization endpoint, an
24 // authorization transaction is initiated. To complete the transaction, the
25 // user must authenticate and approve the authorization request. Because this
26 // may involve multiple HTTP request/response exchanges, the transaction is
27 // stored in the session.
29 // An application must supply serialization functions, which determine how the
30 // client object is serialized into the session. Typically this will be a
31 // simple matter of serializing the client's ID, and deserializing by finding
32 // the client by ID from the database.
34 server.serializeClient(function (client, done) {
37 return done(null, client);
40 server.deserializeClient(function (client, done) {
43 db.clients.find(client, function (err, foundClient) {
44 if (err) { return done(err); }
45 return done(null, foundClient);
49 // Register supported grant types.
51 // OAuth 2.0 specifies a framework that allows users to grant client
52 // applications limited access to their protected resources. It does this
53 // through a process of the user granting access, and the client exchanging
54 // the grant for an access token.
56 // Grant authorization codes. The callback takes the `client` requesting
57 // authorization, the `redirectURI` (which is used as a verifier in the
58 // subsequent exchange), the authenticated `user` granting access, and
59 // their response, which contains approved scope, duration, etc. as parsed by
60 // the application. The application issues a code, which is bound to these
61 // values, and will be exchanged for an access token.
63 server.grant(oauth2orize.grant.code(function (client, redirectURI, user, ares, done) {
66 if (!client || !user || !redirectURI || !ares) { return done(null, false); }
68 // Generate the auth code.
69 var code = utils.generateUUID(),
70 salt = '$2a$10$' + client.get("clientID").replace(/[^a-zA-Z0-9]/g, "").substring(0, 22),
71 codehash = X.bcrypt.hashSync(code, salt);
73 // The authCode can be used to get a refreshToken and accessToken. We bcrypt the authCode
74 // so if our database is ever compromised, the stored authCode hashes are worthless.
76 if (!Array.isArray(ares.scope)) { ares.scope = [ ares.scope ]; }
78 // Save auth data to the database.
79 db.authorizationCodes.save(codehash, client.get("clientID"), redirectURI, user.id, ares.scope, function (err) {
84 // Return the code to the client.
89 // Exchange authorization codes for access tokens. The callback accepts the
90 // `client`, which is exchanging `code` and any `redirectURI` from the
91 // authorization request for verification. If these values are validated, the
92 // application issues an access token on behalf of the user who authorized the
95 server.exchange(oauth2orize.exchange.code(function (client, code, redirectURI, done) {
98 if (!client || !code || !redirectURI) { return done(null, false); }
100 // Best practice is to use a random salt in each bcrypt hash. Since we need to query the
101 // database for a valid authCode, we would have to loop through all the hashes
102 // and hash the authCode the client sent using each salt and check for a match.
103 // That could take a lot of CPU if there are 1000's of authCodes. Instead, we will
104 // use known salt we can look up that is also in the request to exchange authCodes.
105 // The salt is the client_id trimmed to 22 characters. Unfortunately, this trade off means
106 // the bcrypt salt will be shared across all authCodes issued for a single client.
108 if (client.get("clientID").length < 22) {
109 console.trace("OAuth 2.0 clientID, ", client.get("clientID"), " is too short to use for bcrypt salt.");
110 return done(new Error("Invalid authorization code."));
113 // bcrypt the code before looking for a matching hash.
114 var salt = '$2a$10$' + client.get("clientID").replace(/[^a-zA-Z0-9]/g, "").substring(0, 22),
115 codehash = X.bcrypt.hashSync(code, salt);
117 db.authorizationCodes.find(codehash, client.get("organization"), function (err, authCode) {
118 if (err) { return done(err); }
119 if (!authCode) { return done(null, false); }
120 if (client.get("clientID") !== authCode.get("clientID")) { return done(new Error("Invalid clientID.")); }
121 if (redirectURI !== authCode.get("redirectURI")) { return done(new Error("Invalid redirectURI.")); }
123 // Now that we've looked up the bcrypt authCode hash, double check that the code
124 // sent by the client actually matches using compareSync() this time.
125 if (!X.bcrypt.compareSync(code, authCode.get("authCode"))) {
126 console.trace("OAuth 2.0 authCode failed bcrypt compare. WTF?? This should not happen.");
127 return done(new Error("Invalid authorization code."));
130 // Auth code is only valid for 10 minutes. Has it expired yet?
131 if ((new Date(authCode.get("authCodeExpires")) - new Date()) < 0) {
133 return done(new Error("Authorization code has expired."));
136 var accessToken = utils.generateUUID(),
137 refreshToken = utils.generateUUID(),
142 expires = new Date(today.getTime() + (60 * 60 * 1000)), // One hour from now.
143 tokenType = 'bearer';
145 // A refreshToken is like a password. It currently never expires and with it, you can
146 // get a new accessToken. We bcrypt the refreshToken so if our database is ever
147 // compromised, the stored refreshToken hashes are worthless.
148 refreshhash = X.bcrypt.hashSync(refreshToken, salt);
150 // The accessToken is only valid for 1 hour and must be sent with each request to
151 // the REST API. The bcrypt hash calculation on each request would be too expensive.
152 // Therefore, we do not need to bcrypt the accessToken, just SHA1 it.
153 accesshash = X.crypto.createHash('sha1').update(privateSalt + accessToken).digest("hex");
155 saveOptions.success = function (model) {
156 if (!model) { return done(null, false); }
159 params.token_type = model.get("tokenType");
160 // Google sends time until expires instead of just the time it expires at, so...
161 params.expires_in = Math.round(((expires - today) / 1000) - 60); // Seconds until the token expires with 60 sec padding.
163 // Send the tokens and params along.
164 return done(null, accessToken, refreshToken, params);
166 saveOptions.error = function (model, err) {
167 return done && done(err);
169 saveOptions.database = client.get("organization");
171 // Set model values and save.
172 authCode.set("state", "Token Issued");
173 authCode.set("authCode", null);
174 authCode.set("authCodeExpires", today);
175 authCode.set("refreshToken", refreshhash);
176 authCode.set("refreshIssued", today);
177 authCode.set("accessToken", accesshash);
178 authCode.set("accessIssued", today);
179 authCode.set("accessExpires", expires);
180 authCode.set("tokenType", tokenType);
181 authCode.set("accessType", "offline"); // Default for now...
183 authCode.save(null, saveOptions);
187 // Exchange a refresh token for a new access tokens. The callback accepts the
188 // `Oauth2client` model, `Oauth2token` model and a done callback. If these
189 // values are valid, the application issues an access token on behalf of the
190 // user who authorized the code.
192 server.exchange(oauth2orize.exchange.refreshToken(function (client, refreshToken, done) {
195 if (!client || !refreshToken) { return done(null, false); }
197 // Best practice is to use a random salt in each bcrypt hash. Since we need to query the
198 // database for a valid refreshToken, we would have to loop through all the hashes
199 // and hash the refreshToken the client sent using each salt and check for a match.
200 // That could take a lot of CPU if there are 1000's of refreshTokens. Instead, we will
201 // use known salt we can look up that is also in the request to use refreshTokens.
202 // The salt is the client_id trimmed to 22 characters. Unfortunately, this trade off means
203 // the bcrypt salt will be shared across all refreshTokens issued for a single client.
205 if (client.get("clientID").length < 22) {
206 console.trace("OAuth 2.0 clientID, ", client.get("clientID"), " is too short to use for bcrypt salt.");
207 return done(new Error("Invalid refresh token."));
210 // bcrypt the refreshToken before looking for a matching hash.
211 var salt = '$2a$10$' + client.get("clientID").replace(/[^a-zA-Z0-9]/g, "").substring(0, 22),
212 refreshhash = X.bcrypt.hashSync(refreshToken, salt);
214 db.accessTokens.findByRefreshToken(refreshhash, client.get("organization"), function (err, token) {
215 if (err) { return done(err); }
216 if (!token) { return done(null, false); }
217 if (client.get("clientID") !== token.get("clientID")) { return done(new Error("Invalid clientID.")); }
219 // Now that we've looked up the bcrypt refreshToken hash, double check that the code
220 // sent by the client actually matches using compareSync() this time.
221 if (!X.bcrypt.compareSync(refreshToken, token.get("refreshToken"))) {
222 console.trace("OAuth 2.0 refreshToken failed bcrypt compare. WTF?? This should not happen.");
223 return done(new Error("Invalid refresh token."));
226 // Refresh tokens do not currently expire, but we might add that feature in the future. Has it expired yet?
227 // TODO - refreshExpires === null means refreshToken doesn't expire. If we change that, determine how to handle null.
228 if (token.get("refreshExpires") && ((new Date(token.get("refreshExpires")) - new Date()) < 0)) {
230 return done(new Error("Refresh token has expired."));
233 var accessToken = utils.generateUUID(),
237 expires = new Date(today.getTime() + (60 * 60 * 1000)); // One hour from now.
239 // The accessToken is only valid for 1 hour and must be sent with each request to
240 // the REST API. The bcrypt hash calculation on each request would be too expensive.
241 // Therefore, we do not need to bcrypt the accessToken, just SHA1 it.
242 accesshash = X.crypto.createHash('sha1').update(privateSalt + accessToken).digest("hex");
244 saveOptions.success = function (model) {
245 if (!model) { return done(null, false); }
248 params.token_type = model.get("tokenType");
249 // Google sends time until expires instead of just the time it expires at, so...
250 params.expires_in = Math.round(((expires - today) / 1000) - 60); // Seconds until the token expires with 60 sec padding.
252 // Send the accessToken and params along.
253 // We do not send the refreshToken because they already have it.
254 return done(null, accessToken, null, params);
256 saveOptions.error = function (model, err) {
257 return done && done(err);
260 saveOptions.database = client.get("organization");
262 // Set model values and save.
263 token.set("state", "Token Refreshed");
264 token.set("accessToken", accesshash);
265 token.set("accessIssued", today);
266 token.set("accessExpires", expires);
268 token.save(null, saveOptions);
272 // Exchange a JSON Web Token (JWT) for a new access tokens. The callback accepts
273 // the `Oauth2client` model, the JWT base64URLencoded header, claimSet and
274 // signature parts and a done callback. If these values are valid, the
275 // application issues an access token on behalf of the user in the JWT `prn`
278 server.exchange('assertion', jwtBearer(function (client, header, claimSet, signature, done) {
281 var data = header + "." + claimSet,
282 pub = client.get("clientX509PubCert"),
283 verifier = X.crypto.createVerify("RSA-SHA256");
285 verifier.update(data);
287 if (verifier.verify(pub, utils.base64urlUnescape(signature), 'base64')) {
288 var accessToken = utils.generateUUID(),
290 decodedHeader = JSON.parse(utils.base64urlDecode(header)),
291 decodedClaimSet = JSON.parse(utils.base64urlDecode(claimSet)),
297 expires = new Date(today.getTime() + (60 * 60 * 1000)), // One hour from now.
298 token = new SYS.Oauth2token();
300 // Verify JWT was formed correctly.
301 if (!decodedHeader || !decodedHeader.alg || !decodedHeader.typ) {
302 return done(new Error("Invalid JWT header."));
304 if (!decodedClaimSet || decodedClaimSet.length < 5 || !decodedClaimSet.iss ||
305 !decodedClaimSet.scope || !decodedClaimSet.aud || !decodedClaimSet.exp ||
306 !decodedClaimSet.iat) {
308 return done(new Error("Invalid JWT claim set."));
311 // These dates will be valid epoch timestamps because of (... * 1000)
312 // don't use for initial check of epoch validity below.
313 expDate = new Date(decodedClaimSet.exp * 1000);
314 iatDate = new Date(decodedClaimSet.iat * 1000);
316 if (((new Date(decodedClaimSet.exp)).getTime() <= 0) || ((new Date(decodedClaimSet.iat)).getTime() <= 0) || // exp && iat are NOT valid epoch timestamps.
317 ((expDate.getTime()) - (iatDate.getTime()) <= 0) || // exp - iat <= 0
318 ((expDate.getTime()) - (iatDate.getTime()) > 3600000) || // exp is more than 1 hour from the iat time.
319 (((iatDate - today) - (10 * 60 * 1000)) > 0) // Great Scott! JWT was issued in the future. 10 minute buffer for clock errors.
322 return done(new Error("Invalid JWT timestamps."));
325 // Is the JWT ClaimSet.iss a valid clientID?
326 if (client.get("clientID") !== decodedClaimSet.iss) {
327 return done(new Error("Invalid JWT iss."));
330 // JWT is only valid for 1 hour. Has it expired yet?
331 if ((expDate - today) < 0) {
332 return done(new Error("JWT has expired."));
335 // Validate decodedClaimSet.prn user and scopes.
336 if (client.get("delegatedAccess") && decodedClaimSet.prn) {
337 db.users.findByUsername(decodedClaimSet.prn, client.get("organization"), function (err, user) {
338 if (err) { return done(new Error("Invalid JWT delegate user.")); }
339 if (!user) { return done(null, false); }
342 jwtScopes = decodedClaimSet.scope.split(separator),
346 if (!Array.isArray(jwtScopes)) { jwtScopes = [ jwtScopes ]; }
348 // Loop through the scope URIs and convert them to org names.
349 _.each(jwtScopes, function (scopeValue, scopeKey, scopeList) {
352 // Get the org from the scope URI e.g. 'dev' from: 'https://mobile.xtuple.com/auth/dev'
353 scope = url.parse(scopeValue, true);
354 scopeOrg = scope.path.split("/")[1];
356 if (user.get("organization") === scopeOrg) {
357 scopes[scopeKey] = scopeOrg;
361 if (scopes.length < 1) {
362 return done(new Error("Invalid JWT scope."));
365 // JWT is valid, create access token, save and return it.
367 // The accessToken is only valid for 1 hour and must be sent with each request to
368 // the REST API. The bcrypt hash calculation on each request would be too expensive.
369 // Therefore, we do not need to bcrypt the accessToken, just SHA1 it.
370 accesshash = X.crypto.createHash('sha1').update(privateSalt + accessToken).digest("hex");
372 saveOptions.success = function (model) {
373 if (!model) { return done(null, false); }
376 params.token_type = model.get("tokenType");
377 // Google sends time until expires instead of just the time it expires at, so...
378 params.expires_in = Math.round(((expires - today) / 1000) - 60); // Seconds until the token expires with 60 sec padding.
380 // Send the accessToken and params along.
381 // We do not send the refreshToken because they already have it.
382 return done(null, accessToken, params);
384 saveOptions.error = function (model, err) {
385 return done && done(err);
388 saveOptions.database = scopes[0];
390 initCallback = function (model, value) {
392 // Now that model is ready, set attributes and save.
393 var tokenAttributes = {
394 clientID: client.get("clientID"),
395 scope: JSON.stringify(scopes),
396 state: "JWT Access Token Issued",
397 approvalPrompt: false,
398 accessToken: accesshash,
400 accessExpires: expires,
402 accessType: "offline",
403 delegate: user.get("username")
406 // Try to save access token data to the database.
407 model.save(tokenAttributes, saveOptions);
409 return done && done(new Error('Cannot save access token. No id set.'));
413 // Register on change of id callback to know when the model is initialized.
414 token.on('change:id', initCallback);
416 // Set model values and save.
417 token.initialize(null, {isNew: true, database: scopes[0]});
420 // No prn, throw error for now.
421 return done(new Error("Invalid JWT. No delegate user."));
423 // TODO - Handle public scopes with no delegatedAccess users if we ever need to.
426 return done(new Error("Invalid JWT. Signature verification failed"));
431 // TODO - We need a token revoke endpoint some day.
432 //https://developers.google.com/accounts/docs/OAuth2WebServer#tokenrevoke
435 // user authorization endpoint
437 // `authorization` middleware accepts a `validate` callback which is
438 // responsible for validating the client making the authorization request. In
439 // doing so, is recommended that the `redirectURI` be checked against a
440 // registered value, although security requirements may vary accross
441 // implementations. Once validated, the `done` callback must be invoked with
442 // a `client` instance, as well as the `redirectURI` to which the user will be
443 // redirected after an authorization decision is obtained.
445 // This middleware simply initializes a new authorization transaction. It is
446 // the application's responsibility to authenticate the user and render a dialog
447 // to obtain their approval (displaying details about the client requesting
448 // authorization). We accomplish that here by routing through `ensureLoggedIn()`
449 // first, and rendering the `dialog` view.
451 exports.authorization = [
452 server.authorization(function (clientID, redirectURI, scope, type, done) {
455 // Get the org from the scope URI e.g. 'dev' from: 'https://mobile.xtuple.com/auth/dev'
456 scope = url.parse(scope[0], true);
457 var scopeOrg = scope.path.split("/")[1] || null;
459 db.clients.findByClientId(clientID, scopeOrg, function (err, client) {
460 if (err) { return done(err); }
461 if (!client) { return done(null, false); }
465 // For security purposes, we check that redirectURI provided
466 // by the client matches one registered with the server.
467 _.each(client.get("redirectURIs"), function (value, key, list) {
468 // TODO - When adding the UI interface to allow redirectURI to be saved to the DB,
469 // we need to check and make sure they are https URIs.
471 // Check if the requested redirectURI is in approved client.redirectURIs.
472 if (value.redirectURI && value.redirectURI === redirectURI) {
478 return done(null, client, redirectURI);
480 return done(null, false);
484 function (req, res, next) {
487 // Load the OAuth req data into the session so it can access it on login redirects.
489 req.session.oauth2 = req.oauth2;
493 // TODO - Client should be able to get a token for a userinfo REST call but
494 // not have a selected org. login.ensureLoggedIn() needs to support this.
495 // This would allow a client not to specify a scope, receive an error that includes
496 // the URI to call to get a user's scope/org list: 'https://mobile.xtuple.com/auth/userinfo.xxx'
498 login.ensureLoggedIn({redirectTo: "/"}),
499 function (req, res, next) {
504 rootUrl = req.protocol + "://" + req.host + "/",
505 routes = require('../routes/routes');
507 // Handle the returned scopes list.
508 callback = function (result) {
509 if (result.isError) {
510 return next(new Error("Invalid Request."));
518 "logo": req.oauth2.client.get("clientLogo"),
519 "name": req.oauth2.client.get("clientName")
521 scope = req.session.passport.user.organization;
523 // Loop through the requested OAuth 2.0 scopes and get the descriptions.
524 for (var i = 0; i < req.oauth2.req.scope.length; i++) {
525 if (result.data && result.data.oauth2 && result.data.oauth2.scopes &&
526 result.data.oauth2.scopes[req.oauth2.req.scope[i]]
529 // Add the description for this scope to the array to be displayed by dialog.ejs form.
530 scopes.push(result.data.oauth2.scopes[req.oauth2.req.scope[i]].description);
534 // Render the dialog.ejs form.
535 res.render('dialog', { transactionID: req.oauth2.transactionID, user: req.user.id, client: client, scope: scope, scopes: scopes });
539 if (req.session && req.session.passport && req.session.passport.user && req.session.passport.user.organization) {
540 payload.nameSpace = "XT";
541 payload.type = "Discovery";
543 functionName: "getAuth",
544 parameters: [null, rootUrl]
547 // Get the scopes list from the Discovery Doc.
548 routes.queryDatabase("post", payload, req.session, callback);
550 next(new Error('Invalid OAuth 2.0 scope.'));
556 // user decision endpoint
558 // `decision` middleware processes a user's decision to allow or deny access
559 // requested by a client application. Based on the grant type requested by the
560 // client, the above grant middleware configured above will be invoked to send
564 login.ensureLoggedIn({redirectTo: "/"}),
565 server.decision(function (req, next) {
568 // Add the approved scope/org to req.oauth2.res.
571 if (req.session && req.session.passport && req.session.passport.user && req.session.passport.user.organization) {
572 ares.scope = req.session.passport.user.organization;
574 // Oauth 2.0 has been approved. Remove it from the session so the user
575 // can login to the app normally again.
576 delete req.session.oauth2;
578 return next(null, ares);
580 return next(new Error('Invalid OAuth 2.0 scope.'));
588 // `token` middleware handles client requests to exchange authorization grants
589 // for access tokens. Based on the grant type being exchanged, the above
590 // exchange middleware will be invoked to handle the request. Clients must
591 // authenticate when making requests to this endpoint.
594 passport.authenticate(['basic', 'oauth2-client-password', 'oauth2-jwt-bearer'], { session: false }),
596 server.errorHandler()