Merge remote-tracking branch 'xtuple/master' into 18488
[xtuple] / node-datasource / routes / auth.js
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 */
3 /*global X:true, _:true, SYS:true */
4
5 (function () {
6   "use strict";
7
8   /**
9     @name Auth
10     @class Auth
11     */
12   var passport = require('passport'),
13       url = require('url');
14
15   /**
16     Receives user authentication credentials and have passport do the authentication.
17    */
18   exports.login = [
19     //passport.authenticate('local', { successReturnToOrRedirect: '/login/scope', failureRedirect: '/', failureFlash: 'Invalid username or password.' }),
20     passport.authenticate('local', { failureRedirect: '/?login=fail' }),
21     function (req, res, next) {
22
23       if (req && req.session && !req.session.oauth2 && req.session.passport && req.session.passport.user && req.session.passport.user.organization) {
24         res.redirect("/" + req.session.passport.user.organization + '/app');
25         //next();
26       } else {
27         exports.scopeForm(req, res, next);
28       }
29     }
30   ];
31
32   /**
33     Renders the login form
34    */
35   exports.loginForm = function (req, res) {
36     var message = [];
37
38     if (req.query && req.query.login && req.query.login === 'fail') {
39       message = ["Invalid username or password."];
40     }
41
42     res.render('login', { message: message, databases: X.options.datasource.databases });
43   };
44
45   /**
46     Logs out user by removing the session and sending the user to the login screen.
47    */
48   exports.logout = function (req, res) {
49     if (req.session.passport) {
50       // Make extra sure passport is empty.
51       req.session.passport = null;
52     }
53
54     if (req.session) {
55       // Kill the whole session, db, cache and all.
56       req.session.destroy(function () {});
57     }
58
59     if (req.path.split("/")[1]) {
60       res.clearCookie(req.path.split("/")[1] + ".sid");
61     }
62
63     req.logout();
64     res.redirect('/');
65   };
66
67   /**
68     Receives a request telling us which organization a user has selected
69     to log into. Note that we don't trust the client; we check
70     to make sure that the user actually belongs to that organization.
71    */
72   exports.scope = function (req, res, next) {
73     var userId = req.session.passport.user.id,
74       selectedOrg = req.body.org,
75       user = new SYS.User(),
76       options = {};
77
78     options.success = function (response) {
79       var privs,
80           userOrg,
81           userName;
82
83       if (response.length === 0) {
84         if (req.session && req.session.oauth2 && req.session.oauth2.redirectURI) {
85           X.log("OAuth 2.0 User %@ has no business trying to log in to organization %@.".f(userId, selectedOrg));
86           res.redirect(req.session.oauth2.redirectURI + '?error=access_denied');
87           return;
88         }
89
90         X.log("User %@ has no business trying to log in to organization %@.".f(userId, selectedOrg));
91         res.redirect('/' + selectedOrg + '/logout');
92         return;
93       } else if (response.length > 1) {
94         X.log("More than one User: %@ exists.".f(userId));
95         res.redirect('/' + selectedOrg + '/logout');
96         return;
97       }
98
99       // We can now trust this user's request to log in to this organization.
100
101       // Update the session store row to add the org choice and username.
102       // Note: Updating this object magically persists the data into the SessionStore table.
103
104       //privs = _.map(response.get("privileges"), function (privAss) {
105       //  return privAss.privilege.name;
106       //});
107
108       //_.each(response.get('organizations'), function (orgValue, orgKey, orgList) {
109       //  if (orgValue.name === selectedOrg) {
110       //    userOrg = orgValue.name;
111       //    userName = orgValue.username;
112       //  }
113       //});
114
115       //if (!userOrg || !userName) {
116       if (!response.get("username")) {
117         // This shouldn't happen.
118         X.log("User %@ has no business trying to log in to organization %@.".f(userId, selectedOrg));
119         res.redirect('/' + selectedOrg + '/logout');
120         return;
121       }
122
123       //req.session.passport.user.globalPrivileges = privs;
124       req.session.passport.user.organization = response.get("organization");
125       req.session.passport.user.username = response.get("username");
126
127 // TODO - req.oauth probably isn't enough here, but it's working 2013-03-15...
128       // If this is an OAuth 2.0 login with only 1 org.
129       if (req.oauth2) {
130         return next();
131       }
132
133       // If this is an OAuth 2.0 login with more than 1 org.
134       if (req.session.returnTo) {
135         res.redirect(req.session.returnTo);
136       } else {
137         // Redirect to start loading the client app.
138         res.redirect('/' + selectedOrg + '/app');
139       }
140     };
141
142     options.error = function (model, error) {
143       X.log("userorg fetch error", error);
144       res.redirect('/' + selectedOrg + '/logout');
145       return;
146     };
147
148
149     // The user id we're searching for.
150     options.id = userId;
151
152     // The user under whose authority the query is run.
153     options.username = X.options.databaseServer.user;
154     options.database = selectedOrg;
155
156     // Verify that the org is valid for the user.
157     user.fetch(options);
158   };
159
160   /**
161     Loads the form to let the user choose their organization. If there's only one
162     organization for the user we choose for them.
163    */
164   exports.scopeForm = function (req, res, next) {
165     var organizations = [],
166         scope,
167         scopes = [];
168
169     try {
170       organizations = _.map(req.user.get("organizations"), function (org) {
171         return org.name;
172       });
173     } catch (error) {
174       // Prevent unauthorized access.
175       res.redirect('/');
176       return;
177     }
178
179     // If this is an OAuth 2.0 login req, try and get the org from the requested scope.
180     if (req.session && req.session.oauth2) {
181
182       if (req.session.oauth2.req && req.session.oauth2.req.scope && req.session.oauth2.req.scope.length > 0) {
183         // Loop through the scope URIs and convert them to org names.
184         _.each(req.session.oauth2.req.scope, function (value, key, list) {
185           var org;
186
187           // Get the org from the scope URI e.g. 'dev' from: 'https://mobile.xtuple.com/auth/dev'
188           scope = url.parse(value, true);
189           org = scope.path.split("/")[1] || null;
190
191           // TODO - Still need more work to support userinfo calls.
192           // See node-datasource/oauth2/oauth2.js authorization.
193
194           // The scope 'https://mobile.xtuple.com/auth/userinfo.xxx' can be used to make userinfo
195           // REST calls and is not a valid org scope, we'll skip it here.
196           if (org && org.indexOf('userinfo') === -1) {
197             scopes[key] = org;
198           }
199         });
200
201         // If we only have one scope/org sent, choose it for this request.
202         if (scopes.length === 1) {
203           req.body.org = scopes[0];
204           exports.scope(req, res, next);
205           return;
206         }
207
208         // TODO - Multiple scopes sent.
209         // Do we want to let them select an org or respond with error and scopeList?
210         // It depends on the scenario. Some support user interaction and can select an org, others
211         // do not and should get an error.
212
213       }
214
215       // TODO - No scope is sent.
216       // Do we want to let them select an org or respond with error and scopeList?
217       // It depends on the scenario. Some support user interaction and can select an org, others
218       // do not and should get an error.
219
220     }
221
222     // Below will handle OAuth "TODO - Multiple scopes sent", "TODO - No scope is sent." above for now.
223
224     // Choose an org automatically if there's only one for this user.
225     if (organizations.length === 1) {
226       req.body.org = organizations[0];
227       exports.scope(req, res, next);
228       return;
229     }
230
231     // Some users may not have any orgs. They should not get this far.
232     if (organizations.length === 0) {
233       X.err("User: %@ shall not pass, they have no orgs to select.".f(req.session.passport.user.id));
234       req.flash('orgerror', 'You have not been assigned to any organizations.');
235     }
236
237     // We've got nothing, let the user choose their scope/org.
238     res.render('scope', { organizations: organizations.sort(), message: req.flash('orgerror') });
239   };
240 }());