| 1 | | /*! |
| 2 | | * userauth - lib/userauth.js |
| 3 | | * Copyright(c) 2012 fengmk2 <fengmk2@gmail.com> |
| 4 | | * MIT Licensed |
| 5 | | */ |
| 6 | | |
| 7 | 1 | "use strict"; |
| 8 | | |
| 9 | | /** |
| 10 | | * Module dependencies. |
| 11 | | */ |
| 12 | | |
| 13 | 1 | var urlparse = require('url').parse; |
| 14 | | |
| 15 | | /** |
| 16 | | * Send redirect response. |
| 17 | | * |
| 18 | | * @param {Response} res, http.Response instance |
| 19 | | * @param {String} url, redirect URL |
| 20 | | * @param {Number|String} status, response status code, default is `302` |
| 21 | | * @api public |
| 22 | | */ |
| 23 | 1 | var redirect = function (res, url, status) { |
| 24 | 57 | status = status === 301 ? 301 : 302; |
| 25 | 57 | res.setHeader('Location', url); |
| 26 | | |
| 27 | 57 | var body = ''; |
| 28 | 57 | var accept = (res.req.headers && res.req.headers.accept) || ''; |
| 29 | 57 | if (accept.indexOf('json') >= 0) { |
| 30 | 4 | status = 401; |
| 31 | 4 | res.setHeader('Content-Type', 'application/json'); |
| 32 | 4 | body = JSON.stringify({ error: '401 Unauthorized' }); |
| 33 | | } |
| 34 | 57 | res.statusCode = status; |
| 35 | 57 | res.end(body); |
| 36 | | }; |
| 37 | | |
| 38 | 1 | function formatReferer(req, pathname) { |
| 39 | 21 | var query = req.query; |
| 40 | 21 | if (!query) { |
| 41 | 21 | query = urlparse(req.originalUrl || req.url, true).query; |
| 42 | | } |
| 43 | 21 | var referer = query.redirect || req.headers.referer || '/'; |
| 44 | 21 | referer = typeof referer === 'string' ? referer : '/'; |
| 45 | 21 | if (referer[0] !== '/') { |
| 46 | | // ignore http://xxx/abc |
| 47 | 4 | referer = '/'; |
| 48 | 17 | } else if (referer.indexOf(pathname) >= 0) { |
| 49 | 2 | referer = '/'; |
| 50 | | } |
| 51 | 21 | return referer; |
| 52 | | } |
| 53 | | |
| 54 | 1 | function login(options) { |
| 55 | 2 | return function (req, res, next) { |
| 56 | 10 | req.session._loginReferer = formatReferer(req, options.loginPath); |
| 57 | 10 | var currentURL = 'http://' + req.headers.host + options.loginCallbackPath; |
| 58 | 10 | var loginURL = options.loginURLForamter(currentURL); |
| 59 | 10 | redirect(res, loginURL); |
| 60 | | }; |
| 61 | | } |
| 62 | | |
| 63 | 1 | function loginCallback(options) { |
| 64 | 2 | return function (req, res, next) { |
| 65 | 26 | var referer = req.session._loginReferer || '/'; |
| 66 | 26 | var user = req.session[options.userField]; |
| 67 | 26 | if (user) { |
| 68 | | // already login |
| 69 | 10 | return redirect(res, referer); |
| 70 | | } |
| 71 | 16 | options.getUser(req, function (err, user) { |
| 72 | 16 | if (err) { |
| 73 | | // 5. get user error, next(err) |
| 74 | 1 | return next(err); |
| 75 | | } |
| 76 | | |
| 77 | 15 | if (!user) { |
| 78 | 5 | return redirect(res, referer); |
| 79 | | } |
| 80 | | |
| 81 | 10 | options.loginCallback(req, user, function (err, loginUser, redirectURL) { |
| 82 | 10 | if (err) { |
| 83 | 1 | return next(err); |
| 84 | | } |
| 85 | | |
| 86 | 9 | req.session[options.userField] = loginUser; |
| 87 | 9 | if (redirectURL) { |
| 88 | 1 | referer = redirectURL; |
| 89 | | } |
| 90 | 9 | redirect(res, referer); |
| 91 | | }); |
| 92 | | }); |
| 93 | | }; |
| 94 | | } |
| 95 | | |
| 96 | 1 | function logout(options) { |
| 97 | 2 | return function (req, res, next) { |
| 98 | 11 | var referer = formatReferer(req, options.logoutPath); |
| 99 | 11 | var user = req.session[options.userField]; |
| 100 | 11 | if (!user) { |
| 101 | 7 | return redirect(res, referer); |
| 102 | | } |
| 103 | | |
| 104 | 4 | options.logoutCallback(req, res, user, function (err, redirectURL) { |
| 105 | 4 | if (err) { |
| 106 | 1 | return next(err); |
| 107 | | } |
| 108 | | |
| 109 | 3 | req.session[options.userField] = null; |
| 110 | 3 | if (redirectURL) { |
| 111 | 1 | referer = redirectURL; |
| 112 | | } |
| 113 | 3 | redirect(res, referer); |
| 114 | | }); |
| 115 | | }; |
| 116 | | } |
| 117 | | |
| 118 | | /** |
| 119 | | * User auth middleware. |
| 120 | | * |
| 121 | | * @param {Regex|Function(pathname, req)} match, detect which url need to check user auth. |
| 122 | | * @param {Object} [options] |
| 123 | | * - {Function(url)} loginURLForamter, format the login url. |
| 124 | | * - {String} [loginPath], default is '/login'. |
| 125 | | * - {String} [loginCallbackPath], default is `options.loginPath + '/callback'`. |
| 126 | | * - {String} [logoutPath], default is '/logout'. |
| 127 | | * - {String} [userField], logined user field name on `req.session`, default is 'user', `req.session.user`. |
| 128 | | * - {Function(req, callback)} getUser, get user function, must get user info with `req`. |
| 129 | | * - {Function(req, user, callback)} [loginCallback], you can handle user login logic here. |
| 130 | | * - {Function(err, user, redirectURL)} callback |
| 131 | | * - {Function(req)} [loginCheck], return true meaning logined. default is `true`. |
| 132 | | * - {Function(req, res, user, callback)} [logoutCallback], you can handle user logout logic here. |
| 133 | | * - {Function(err, redirectURL)} callback |
| 134 | | * @return {Function(req, res, next)} userauth middleware |
| 135 | | * @public |
| 136 | | */ |
| 137 | 1 | module.exports = function userauth(match, options) { |
| 138 | 2 | options = options || {}; |
| 139 | 2 | options.userField = options.userField || 'user'; |
| 140 | 2 | options.loginPath = options.loginPath || '/login'; |
| 141 | 2 | options.loginCallbackPath = options.loginCallbackPath || options.loginPath + '/callback'; |
| 142 | 2 | options.logoutPath = options.logoutPath || '/logout'; |
| 143 | 2 | options.loginURLForamter = options.loginURLForamter; |
| 144 | 2 | options.getUser = options.getUser; |
| 145 | | |
| 146 | 2 | var defaultRedirectHandler = function (req, res, nextHandler) { |
| 147 | 4 | nextHandler(); |
| 148 | | }; |
| 149 | 2 | options.redirectHandler = options.redirectHandler || defaultRedirectHandler; |
| 150 | | |
| 151 | 2 | var needLogin = match; |
| 152 | | |
| 153 | 2 | if (typeof match === 'string') { |
| 154 | 0 | match = new RegExp('^' + match); |
| 155 | | } |
| 156 | | |
| 157 | 2 | if (match instanceof RegExp) { |
| 158 | 2 | needLogin = function (pathname, req) { |
| 159 | 25 | return match.test(pathname); |
| 160 | | }; |
| 161 | 0 | } else if (typeof match !== 'function') { |
| 162 | 0 | needLogin = function () {}; |
| 163 | | } |
| 164 | | |
| 165 | 2 | var defaultLoginCallback = function (req, user, callback) { |
| 166 | 1 | return callback(null, user, null); |
| 167 | | }; |
| 168 | 2 | var defaultLogoutCallback = function (req, res, user, callback) { |
| 169 | 1 | return callback(null, null); |
| 170 | | }; |
| 171 | | |
| 172 | 2 | options.loginCallback = options.loginCallback || defaultLoginCallback; |
| 173 | 2 | options.logoutCallback = options.logoutCallback || defaultLogoutCallback; |
| 174 | | // options.loginCheck = options.loginCheck; |
| 175 | | |
| 176 | 2 | var loginHandler = login(options); |
| 177 | 2 | var loginCallbackHandler = loginCallback(options); |
| 178 | 2 | var logoutHandler = logout(options); |
| 179 | | |
| 180 | | /** |
| 181 | | * login flow: |
| 182 | | * |
| 183 | | * 1. unauth user, redirect to `$loginPath?redirect=$currentURL` |
| 184 | | * 2. user visit `$loginPath`, redirect to `options.loginURLForamter()` return login url. |
| 185 | | * 3. user visit $loginCallbackPath, handler login callback logic. |
| 186 | | * 4. If user login callback check success, will set `req.session[userField]`, |
| 187 | | * and redirect to `$currentURL`. |
| 188 | | * 5. If login check callback error, next(err). |
| 189 | | * 6. user visit `$logoutPath`, set `req.session[userField] = null`, and redirect back. |
| 190 | | */ |
| 191 | | |
| 192 | 2 | return function authMiddleware(req, res, next) { |
| 193 | 72 | if (!res.req) { |
| 194 | 72 | res.req = req; |
| 195 | | } |
| 196 | | |
| 197 | 72 | var url = req.originalUrl || req.url; |
| 198 | 72 | var urlinfo = urlparse(url); |
| 199 | | |
| 200 | | // 2. GET $loginPath |
| 201 | 72 | if (urlinfo.pathname === options.loginPath) { |
| 202 | 10 | return loginHandler(req, res, next); |
| 203 | | } |
| 204 | | |
| 205 | | // 3. GET $loginCallbackPath |
| 206 | 62 | if (urlinfo.pathname === options.loginCallbackPath) { |
| 207 | 26 | return loginCallbackHandler(req, res, next); |
| 208 | | } |
| 209 | | |
| 210 | | // 6. GET $logoutPath |
| 211 | 36 | if (urlinfo.pathname === options.logoutPath) { |
| 212 | 11 | return logoutHandler(req, res, next); |
| 213 | | } |
| 214 | | |
| 215 | 25 | if (!needLogin(urlinfo.pathname, req)) { |
| 216 | 5 | return next(); |
| 217 | | } |
| 218 | | |
| 219 | 20 | if (req.session[options.userField] && (!options.loginCheck || options.loginCheck(req))) { |
| 220 | | // 4. user logined, next() handler |
| 221 | 1 | return next(); |
| 222 | | } |
| 223 | | |
| 224 | | // check user logined or not |
| 225 | | // If user auth token vaild, just getUser() directly |
| 226 | 19 | options.getUser(req, function (err, user) { |
| 227 | 19 | if (err) { |
| 228 | 1 | return next(err); |
| 229 | | } |
| 230 | | |
| 231 | 18 | if (!user) { |
| 232 | | // 1. redirect to $loginPath |
| 233 | 13 | var nextHandler = function () { |
| 234 | 12 | var redirectURL = url; |
| 235 | 12 | try { |
| 236 | 12 | redirectURL = encodeURIComponent(redirectURL); |
| 237 | | } catch (e) { |
| 238 | | // URIError: URI malformed |
| 239 | | // use source url |
| 240 | | } |
| 241 | 12 | redirect(res, options.loginPath + '?redirect=' + redirectURL); |
| 242 | | }; |
| 243 | 13 | return options.redirectHandler(req, res, nextHandler); |
| 244 | | } |
| 245 | | |
| 246 | 5 | options.loginCallback(req, user, function (err, loginUser, redirectURL) { |
| 247 | 5 | if (err) { |
| 248 | 1 | return next(err); |
| 249 | | } |
| 250 | | |
| 251 | 4 | req.session[options.userField] = loginUser; |
| 252 | 4 | if (redirectURL) { |
| 253 | 1 | return redirect(res, redirectURL); |
| 254 | | } |
| 255 | 3 | next(); |
| 256 | | }); |
| 257 | | }); |
| 258 | | |
| 259 | | }; |
| 260 | | }; |