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