├── .DS_Store ├── .babelrc ├── .gitignore ├── LICENSE ├── README.md ├── index.js ├── lib ├── index.js └── test.js ├── package.json └── src ├── index.js └── test.js /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/robin98sun/koa-jwt-redis-session/fa0c42e71e936da65bff68ebcb138e5edf2720fe/.DS_Store -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "es2016-node5"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Lyall Sun 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #JWT Redis Session for Koa 2 2 | 3 | Pure JWT implementation using Redis as session storage for Koa 2, without any cookies 4 | 5 | Quick Start 6 | =========== 7 | As middleware: 8 | 9 | ```javascript 10 | const koa = require('koa'), 11 | bodyParser = require('koa-bodyparser'), 12 | session = require('koa-jwt-redis-session') 13 | // import session from 'koa-jwt-redis-session' 14 | 15 | const app = new koa() 16 | app.use(bodyParser()) 17 | 18 | app.use(session.default()) 19 | 20 | // If using import 21 | // app.use(session()) 22 | 23 | app.use(async function(ctx, next){ 24 | let views = ctx.session.views || 0 25 | ctx.session.views = ++views 26 | try{ 27 | ctx.body = {views: ctx.session.views} 28 | await next() 29 | }catch(ex){ 30 | console.error('something wrong:', ex) 31 | ctx.status = 500 32 | ctx.body = 'something wrong' 33 | } 34 | }) 35 | 36 | app.listen(3333) 37 | ``` 38 | 39 | As a function: 40 | 41 | ```javascript 42 | // After used as middleware 43 | // Somewhere when using as backdore 44 | import {createSession, authoriseRequest} from 'koa-jwt-redis-session' 45 | 46 | let openDoorHandler = async (ctx, next)=>{ 47 | let userObj = {account: 'sneaky', password: 'open_the_back_door'}; 48 | let token = await createSession(ctx, userObj); 49 | ctx.body = token; 50 | // Token is in JSON format: {token: ..... , expiresIn: 3600} 51 | // expiresIn is the expire time in seconds, default is 3600 52 | } 53 | 54 | let guardHandler = async (ctx, next)=>{ 55 | let user = await authoriseRequest(ctx); 56 | if( user != undefined){ 57 | ctx.body = user; 58 | }else{ 59 | ctx.throw(new Error('Unauthorized')); 60 | } 61 | } 62 | 63 | ``` 64 | 65 | Options 66 | ======= 67 | 68 | When creating session instance, you can pass in an option object 69 | 70 | ```javascript 71 | const sessionOptions = { 72 | // ...... 73 | } 74 | app.use(session.default(sessionOptions)) 75 | 76 | // If using import 77 | app.use(session(sessionOptions)) 78 | ``` 79 | 80 | Here is the default option values 81 | --------------------------------- 82 | 83 | ```javascript 84 | { 85 | jwt: { 86 | contentType: 'application/json', 87 | charset: 'utf-8', 88 | secret: 'koa-jwt-redis-session' + new Date().getTime(), 89 | authPath: '/authorize', 90 | registerPath: '/register', 91 | refreshTokenPath: '/refreshToken', 92 | expiresIn: 3600, 93 | accountKey: 'account', 94 | passwordKey: 'password', 95 | authHandler: function (account, password) { 96 | if (account && password) { 97 | let user = {}; 98 | user[accountKey] = account; 99 | return user; 100 | } 101 | else return false; 102 | }, 103 | registerHandler: function (account, password) { 104 | if (account && password) { 105 | let user = {}; 106 | user[accountKey] = account; 107 | return user; 108 | } 109 | else return false; 110 | } 111 | }, 112 | session: { 113 | sessionKey: 'session', 114 | sidKey: 'koa:sess', 115 | }, 116 | redis: { 117 | port: 6379, 118 | host: '127.0.0.1', 119 | db: 0, 120 | ttl: 3600, 121 | options: {} 122 | } 123 | } 124 | ``` 125 | 126 | Action flow 127 | =========== 128 | 129 | 1. Anonymous client post JSON user credential information `{ account: "...", password: "..." }` to `/register` to register an account, 130 | 2. or post to `/authorize` to get authorization 131 | 3. Client get token in JSON like `{ token: "...", expiresIn: 3600 }`, or an `401` error if not authorized 132 | 4. From then on, client send every request by the http header: `Authorization: Bearer `, 133 | 5. or client would get `401` error if not authorized or *token expired* 134 | 6. On the server side, afterward middlewares can operate `ctx.session` as will 135 | 136 | 137 | Enjoy! 138 | ====== -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/index.js') -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.authoriseRequest = exports.createSession = undefined; 7 | 8 | var _getPrototypeOf = require('babel-runtime/core-js/object/get-prototype-of'); 9 | 10 | var _getPrototypeOf2 = _interopRequireDefault(_getPrototypeOf); 11 | 12 | var _possibleConstructorReturn2 = require('babel-runtime/helpers/possibleConstructorReturn'); 13 | 14 | var _possibleConstructorReturn3 = _interopRequireDefault(_possibleConstructorReturn2); 15 | 16 | var _inherits2 = require('babel-runtime/helpers/inherits'); 17 | 18 | var _inherits3 = _interopRequireDefault(_inherits2); 19 | 20 | var _typeof2 = require('babel-runtime/helpers/typeof'); 21 | 22 | var _typeof3 = _interopRequireDefault(_typeof2); 23 | 24 | var _keys = require('babel-runtime/core-js/object/keys'); 25 | 26 | var _keys2 = _interopRequireDefault(_keys); 27 | 28 | var _stringify = require('babel-runtime/core-js/json/stringify'); 29 | 30 | var _stringify2 = _interopRequireDefault(_stringify); 31 | 32 | var _classCallCheck2 = require('babel-runtime/helpers/classCallCheck'); 33 | 34 | var _classCallCheck3 = _interopRequireDefault(_classCallCheck2); 35 | 36 | var _createClass2 = require('babel-runtime/helpers/createClass'); 37 | 38 | var _createClass3 = _interopRequireDefault(_createClass2); 39 | 40 | var _regenerator = require('babel-runtime/regenerator'); 41 | 42 | var _regenerator2 = _interopRequireDefault(_regenerator); 43 | 44 | var _asyncToGenerator2 = require('babel-runtime/helpers/asyncToGenerator'); 45 | 46 | var _asyncToGenerator3 = _interopRequireDefault(_asyncToGenerator2); 47 | 48 | var _ioredis = require('ioredis'); 49 | 50 | var _ioredis2 = _interopRequireDefault(_ioredis); 51 | 52 | var _jsonwebtoken = require('jsonwebtoken'); 53 | 54 | var _jsonwebtoken2 = _interopRequireDefault(_jsonwebtoken); 55 | 56 | var _thunkify = require('thunkify'); 57 | 58 | var _thunkify2 = _interopRequireDefault(_thunkify); 59 | 60 | var _uid = require('uid2'); 61 | 62 | var _uid2 = _interopRequireDefault(_uid); 63 | 64 | var _co = require('co'); 65 | 66 | var _co2 = _interopRequireDefault(_co); 67 | 68 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 69 | 70 | var debug = require('debug')('koa-jwt-redis-session'); 71 | 72 | var DEBUG_LOG_HEADER = '[koa-jwt-redis-session]'; 73 | var EXPIRES_IN_SECONDS = 60 * 60; 74 | 75 | // Options 76 | // JWT Options 77 | var jwtOptions = void 0, 78 | contentType = void 0, 79 | charset = void 0, 80 | secret = void 0, 81 | authPath = void 0, 82 | registerPath = void 0, 83 | expiresIn = void 0, 84 | accountKey = void 0; 85 | var passwordKey = void 0, 86 | authHandler = void 0, 87 | registerHandler = void 0, 88 | jwtOpt = void 0, 89 | refreshTokenPath = void 0; 90 | // Session 91 | var sessionKey = void 0, 92 | sidKey = void 0, 93 | sessOpt = void 0; 94 | // Redis Options 95 | var redisOptions = void 0, 96 | redisStore = void 0, 97 | store = void 0; 98 | 99 | function parseOptions(opts) { 100 | // Options 101 | var options = opts || {}; 102 | debug('Parsing options:', options); 103 | // JWT Options 104 | jwtOptions = options.jwt || {}; 105 | contentType = jwtOptions.contentType || 'application/json'; 106 | charset = jwtOptions.charset || 'utf-8'; 107 | secret = jwtOptions.secret || 'koa-jwt-redis-session' + new Date().getTime(); 108 | authPath = jwtOptions.authPath || '/authorize'; 109 | registerPath = jwtOptions.registerPath || '/register'; 110 | refreshTokenPath = jwtOptions.refreshTokenPath || '/refreshToken'; 111 | expiresIn = jwtOptions.expiresIn || EXPIRES_IN_SECONDS; 112 | accountKey = jwtOptions.accountKey || 'account'; 113 | passwordKey = jwtOptions.passwordKey || 'password'; 114 | authHandler = jwtOptions.authHandler || function (account, password) { 115 | if (account && password) { 116 | var user = {}; 117 | user[accountKey] = account; 118 | return user; 119 | } 120 | return false; 121 | }; 122 | registerHandler = jwtOptions.registerHandler || function (account, password) { 123 | if (account && password) { 124 | var user = {}; 125 | user[accountKey] = account; 126 | return user; 127 | } 128 | return false; 129 | }; 130 | jwtOpt = { expiresIn: expiresIn }; 131 | // Session 132 | var sessionOptions = options.session || {}; 133 | sessionKey = sessionOptions.sessionKey || 'session'; 134 | sidKey = sessionOptions.sidKey || 'koa:sess'; 135 | sessOpt = { sidKey: sidKey }; 136 | // Redis Options 137 | redisOptions = options.redis || {}; 138 | redisStore = new RedisStore(redisOptions); 139 | store = redisStore; 140 | } 141 | 142 | var createSession = function () { 143 | var _ref = (0, _asyncToGenerator3.default)(_regenerator2.default.mark(function _callee(ctx, user) { 144 | var sess, token; 145 | return _regenerator2.default.wrap(function _callee$(_context) { 146 | while (1) { 147 | switch (_context.prev = _context.next) { 148 | case 0: 149 | _context.next = 2; 150 | return Session.create(store, user, sessOpt); 151 | 152 | case 2: 153 | sess = _context.sent; 154 | _context.next = 5; 155 | return _jsonwebtoken2.default.sign(user, secret, jwtOpt); 156 | 157 | case 5: 158 | token = _context.sent; 159 | 160 | ctx[sessionKey] = sess; 161 | debug('Generated token:', token); 162 | return _context.abrupt('return', { token: token, expiresIn: expiresIn }); 163 | 164 | case 9: 165 | case 'end': 166 | return _context.stop(); 167 | } 168 | } 169 | }, _callee, undefined); 170 | })); 171 | 172 | return function createSession(_x, _x2) { 173 | return _ref.apply(this, arguments); 174 | }; 175 | }(); 176 | 177 | var authoriseRequest = function () { 178 | var _ref2 = (0, _asyncToGenerator3.default)(_regenerator2.default.mark(function _callee2(ctx) { 179 | var user, authComponents; 180 | return _regenerator2.default.wrap(function _callee2$(_context2) { 181 | while (1) { 182 | switch (_context2.prev = _context2.next) { 183 | case 0: 184 | //if(ctx.header.authorization){ 185 | user = null; 186 | 187 | if (!(ctx && ctx.header && ctx.header.authorization)) { 188 | _context2.next = 13; 189 | break; 190 | } 191 | 192 | authComponents = ctx.header.authorization.split(' '); 193 | 194 | if (!(authComponents.length === 2 && authComponents[0] === 'Bearer')) { 195 | _context2.next = 13; 196 | break; 197 | } 198 | 199 | _context2.prev = 4; 200 | _context2.next = 7; 201 | return _jsonwebtoken2.default.verify(authComponents[1], secret, jwtOpt); 202 | 203 | case 7: 204 | user = _context2.sent; 205 | _context2.next = 13; 206 | break; 207 | 208 | case 10: 209 | _context2.prev = 10; 210 | _context2.t0 = _context2['catch'](4); 211 | 212 | debug('Illegal token'); 213 | 214 | case 13: 215 | return _context2.abrupt('return', user); 216 | 217 | case 14: 218 | case 'end': 219 | return _context2.stop(); 220 | } 221 | } 222 | }, _callee2, undefined, [[4, 10]]); 223 | })); 224 | 225 | return function authoriseRequest(_x3) { 226 | return _ref2.apply(this, arguments); 227 | }; 228 | }(); 229 | 230 | function middleware(opts) { 231 | parseOptions(opts); 232 | 233 | // Utilities 234 | function sendToken(ctx, token) { 235 | if (contentType.toLowerCase() === 'application/json') ctx.body = token;else ctx.body = token.token; 236 | } 237 | 238 | // Authorization by JWT 239 | return function () { 240 | var _ref3 = (0, _asyncToGenerator3.default)(_regenerator2.default.mark(function _callee3(ctx, next) { 241 | var user, token, account, password, _user, userObj, _token, _account, _password, _user2, _userObj, _token2, _user3; 242 | 243 | return _regenerator2.default.wrap(function _callee3$(_context3) { 244 | while (1) { 245 | switch (_context3.prev = _context3.next) { 246 | case 0: 247 | _context3.prev = 0; 248 | 249 | ctx.type = contentType + ';' + 'charset=' + charset; 250 | 251 | if (!(ctx.path === refreshTokenPath && ctx.method.toUpperCase() === 'POST')) { 252 | _context3.next = 18; 253 | break; 254 | } 255 | 256 | _context3.next = 5; 257 | return authoriseRequest(ctx); 258 | 259 | case 5: 260 | user = _context3.sent; 261 | 262 | if (!user) { 263 | _context3.next = 15; 264 | break; 265 | } 266 | 267 | delete user.iat, user.exp; 268 | _context3.next = 10; 269 | return createSession(ctx, user); 270 | 271 | case 10: 272 | token = _context3.sent; 273 | 274 | debug('Refreshed token:', token, 'user:', user); 275 | sendToken(ctx, token); 276 | _context3.next = 16; 277 | break; 278 | 279 | case 15: 280 | // ctx.body= 'Authorization failed'; 281 | ctx.status = 401; 282 | 283 | case 16: 284 | _context3.next = 70; 285 | break; 286 | 287 | case 18: 288 | if (!(ctx.path === authPath && ctx.method.toUpperCase() === 'POST' && ctx.request.body[accountKey] && ctx.request.body[passwordKey])) { 289 | _context3.next = 37; 290 | break; 291 | } 292 | 293 | account = ctx.request.body[accountKey]; 294 | password = ctx.request.body[passwordKey]; 295 | 296 | debug('checking authorization:', account, password); 297 | _context3.next = 24; 298 | return authHandler(account, password); 299 | 300 | case 24: 301 | _user = _context3.sent; 302 | 303 | if (!(typeof _user === 'boolean' && _user || Object.prototype.toString.call(_user).toLowerCase() === "[object object]")) { 304 | _context3.next = 34; 305 | break; 306 | } 307 | 308 | userObj = void 0; 309 | 310 | if (typeof _user === 'boolean') { 311 | userObj = {}; 312 | userObj[accountKey] = account; 313 | } else userObj = _user; 314 | _context3.next = 30; 315 | return createSession(ctx, userObj); 316 | 317 | case 30: 318 | _token = _context3.sent; 319 | 320 | sendToken(ctx, _token); 321 | _context3.next = 35; 322 | break; 323 | 324 | case 34: 325 | // ctx.body= 'Authorization failed'; 326 | ctx.status = 401; 327 | 328 | case 35: 329 | _context3.next = 70; 330 | break; 331 | 332 | case 37: 333 | if (!(ctx.path === registerPath && ctx.method.toUpperCase() === 'POST' && ctx.request.body[accountKey] && ctx.request.body[passwordKey])) { 334 | _context3.next = 55; 335 | break; 336 | } 337 | 338 | _account = ctx.request.body[accountKey]; 339 | _password = ctx.request.body[passwordKey]; 340 | _context3.next = 42; 341 | return registerHandler(_account, _password); 342 | 343 | case 42: 344 | _user2 = _context3.sent; 345 | 346 | if (!(typeof _user2 === 'boolean' && _user2 || Object.prototype.toString.call(_user2).toLowerCase() === "[object object]")) { 347 | _context3.next = 52; 348 | break; 349 | } 350 | 351 | _userObj = void 0; 352 | 353 | if (typeof _user2 === 'boolean') { 354 | _userObj = {}; 355 | _userObj[accountKey] = _account; 356 | } else _userObj = _user2; 357 | _context3.next = 48; 358 | return createSession(ctx, _userObj); 359 | 360 | case 48: 361 | _token2 = _context3.sent; 362 | 363 | sendToken(ctx, _token2); 364 | _context3.next = 53; 365 | break; 366 | 367 | case 52: 368 | // ctx.body= 'Authorization failed'; 369 | ctx.status = 401; 370 | 371 | case 53: 372 | _context3.next = 70; 373 | break; 374 | 375 | case 55: 376 | _context3.next = 57; 377 | return authoriseRequest(ctx); 378 | 379 | case 57: 380 | _user3 = _context3.sent; 381 | 382 | if (!_user3) { 383 | _context3.next = 70; 384 | break; 385 | } 386 | 387 | debug('Authorized user:', _user3); 388 | _context3.next = 62; 389 | return Session.create(store, _user3, sessOpt); 390 | 391 | case 62: 392 | ctx[sessionKey] = _context3.sent; 393 | _context3.next = 65; 394 | return next(); 395 | 396 | case 65: 397 | if (!(ctx[sessionKey] == undefined || ctx[sessionKey] === false)) { 398 | _context3.next = 68; 399 | break; 400 | } 401 | 402 | _context3.next = 70; 403 | break; 404 | 405 | case 68: 406 | _context3.next = 70; 407 | return ctx[sessionKey].save(store); 408 | 409 | case 70: 410 | _context3.next = 75; 411 | break; 412 | 413 | case 72: 414 | _context3.prev = 72; 415 | _context3.t0 = _context3['catch'](0); 416 | 417 | if (_context3.t0.name !== 'UnauthorizedError' || _context3.t0.name !== 'JsonWebTokenError') { 418 | debug(DEBUG_LOG_HEADER, '[ERROR] catch something wrong:', _context3.t0); 419 | ctx.status = 500; 420 | if (_context3.t0.message && !ctx.body) ctx.body = _context3.t0.message; 421 | } else { 422 | ctx.status = 401; 423 | } 424 | 425 | case 75: 426 | case 'end': 427 | return _context3.stop(); 428 | } 429 | } 430 | }, _callee3, this, [[0, 72]]); 431 | })); 432 | 433 | return function (_x4, _x5) { 434 | return _ref3.apply(this, arguments); 435 | }; 436 | }(); 437 | } 438 | exports.default = middleware; 439 | exports.createSession = createSession; 440 | exports.authoriseRequest = authoriseRequest; 441 | 442 | // Session Model 443 | 444 | var Session = function () { 445 | function Session(obj) { 446 | (0, _classCallCheck3.default)(this, Session); 447 | 448 | if (!obj) this.isNew = true;else for (var k in obj) { 449 | this[k] = obj[k]; 450 | } 451 | } 452 | 453 | /** 454 | * JSON representation of the session. 455 | * 456 | * @return {Object} 457 | * @api public 458 | */ 459 | 460 | 461 | (0, _createClass3.default)(Session, [{ 462 | key: 'changed', 463 | 464 | 465 | /** 466 | * Check if the session has changed relative to the `prev` 467 | * JSON value from the request. 468 | * 469 | * @param {String} [prev] 470 | * @return {Boolean} 471 | * @api private 472 | */ 473 | value: function changed(prev) { 474 | if (!prev) return true; 475 | this._json = (0, _stringify2.default)(this); 476 | return this._json !== prev; 477 | } 478 | 479 | /** 480 | * Return how many values there are in the session object. 481 | * Used to see if it's "populated". 482 | * 483 | * @return {Number} 484 | * @api public 485 | */ 486 | 487 | }, { 488 | key: 'save', 489 | value: function () { 490 | var _ref4 = (0, _asyncToGenerator3.default)(_regenerator2.default.mark(function _callee4(store) { 491 | return _regenerator2.default.wrap(function _callee4$(_context4) { 492 | while (1) { 493 | switch (_context4.prev = _context4.next) { 494 | case 0: 495 | if (store) { 496 | _context4.next = 2; 497 | break; 498 | } 499 | 500 | return _context4.abrupt('return'); 501 | 502 | case 2: 503 | if (!(store.type === 'redis')) { 504 | _context4.next = 5; 505 | break; 506 | } 507 | 508 | _context4.next = 5; 509 | return store.set(this._sessionId, this.json); 510 | 511 | case 5: 512 | case 'end': 513 | return _context4.stop(); 514 | } 515 | } 516 | }, _callee4, this); 517 | })); 518 | 519 | function save(_x6) { 520 | return _ref4.apply(this, arguments); 521 | } 522 | 523 | return save; 524 | }() 525 | }, { 526 | key: 'json', 527 | get: function get() { 528 | var self = this; 529 | var obj = {}; 530 | 531 | (0, _keys2.default)(this).forEach(function (key) { 532 | if ('isNew' === key) return; 533 | if ('_' === key[0]) return; 534 | obj[key] = self[key]; 535 | }); 536 | 537 | return obj; 538 | } 539 | }, { 540 | key: 'string', 541 | get: function get() { 542 | return this._json || (0, _stringify2.default)(this.json); 543 | } 544 | }, { 545 | key: 'length', 546 | get: function get() { 547 | return (0, _keys2.default)(this.toJSON()).length; 548 | } 549 | 550 | /** 551 | * populated flag, which is just a boolean alias of .length. 552 | * 553 | * @return {Boolean} 554 | * @api public 555 | */ 556 | 557 | }, { 558 | key: 'populated', 559 | get: function get() { 560 | return !!this.length; 561 | } 562 | }], [{ 563 | key: 'generateSessionId', 564 | value: function generateSessionId(header) { 565 | if (!header) { 566 | return (0, _uid2.default)(24); 567 | } else { 568 | return header + ":" + (0, _uid2.default)(24); 569 | } 570 | } 571 | 572 | /** 573 | * Create a session instance 574 | * @param store 575 | * @param user 576 | */ 577 | 578 | }, { 579 | key: 'create', 580 | value: function () { 581 | var _ref5 = (0, _asyncToGenerator3.default)(_regenerator2.default.mark(function _callee5(store, user, opts) { 582 | var instance, options, sid, session, _session; 583 | 584 | return _regenerator2.default.wrap(function _callee5$(_context5) { 585 | while (1) { 586 | switch (_context5.prev = _context5.next) { 587 | case 0: 588 | instance = user || {}; 589 | options = opts || { 590 | sidKey: 'sid' 591 | }; 592 | 593 | debug('User for creating session:', instance); 594 | debug('Session options for creating session:', opts); 595 | 596 | if (instance[options.sidKey]) { 597 | _context5.next = 24; 598 | break; 599 | } 600 | 601 | debug('Creating session'); 602 | // Creating 603 | sid = Session.generateSessionId(options.sidKey); 604 | 605 | case 7: 606 | _context5.next = 9; 607 | return store.exists(sid); 608 | 609 | case 9: 610 | if (!_context5.sent) { 611 | _context5.next = 14; 612 | break; 613 | } 614 | 615 | debug('sid', sid, 'exists'); 616 | sid = Session.generateSessionId(options.sidKey); 617 | _context5.next = 7; 618 | break; 619 | 620 | case 14: 621 | debug('new sid:', sid); 622 | user[options.sidKey] = sid; 623 | instance[options.sidKey] = sid; 624 | session = new Session(instance); 625 | 626 | session._sessionId = sid; 627 | _context5.next = 21; 628 | return session.save(store); 629 | 630 | case 21: 631 | return _context5.abrupt('return', session); 632 | 633 | case 24: 634 | debug('Loading session, sid:', instance[options.sidKey]); 635 | // loading 636 | _context5.next = 27; 637 | return store.get(instance[options.sidKey]); 638 | 639 | case 27: 640 | instance = _context5.sent; 641 | 642 | instance._sessionId = instance[options.sidKey]; 643 | _session = new Session(instance); 644 | 645 | debug('loaded session:', _session.json); 646 | return _context5.abrupt('return', _session); 647 | 648 | case 32: 649 | case 'end': 650 | return _context5.stop(); 651 | } 652 | } 653 | }, _callee5, this); 654 | })); 655 | 656 | function create(_x7, _x8, _x9) { 657 | return _ref5.apply(this, arguments); 658 | } 659 | 660 | return create; 661 | }() 662 | }]); 663 | return Session; 664 | }(); 665 | 666 | // Store Base Class 667 | 668 | 669 | var Store = function () { 670 | function Store(opts) { 671 | (0, _classCallCheck3.default)(this, Store); 672 | } 673 | 674 | (0, _createClass3.default)(Store, [{ 675 | key: 'exists', 676 | value: function () { 677 | var _ref6 = (0, _asyncToGenerator3.default)(_regenerator2.default.mark(function _callee8(key) { 678 | var exists, client, value; 679 | return _regenerator2.default.wrap(function _callee8$(_context8) { 680 | while (1) { 681 | switch (_context8.prev = _context8.next) { 682 | case 0: 683 | exists = true; 684 | 685 | if (!(this.type === 'redis')) { 686 | _context8.next = 24; 687 | break; 688 | } 689 | 690 | if (!(!key || !this.client || !this.client.exists)) { 691 | _context8.next = 4; 692 | break; 693 | } 694 | 695 | return _context8.abrupt('return', exists); 696 | 697 | case 4: 698 | client = this.client; 699 | _context8.prev = 5; 700 | _context8.next = 8; 701 | return (0, _co2.default)(_regenerator2.default.mark(function _callee6() { 702 | return _regenerator2.default.wrap(function _callee6$(_context6) { 703 | while (1) { 704 | switch (_context6.prev = _context6.next) { 705 | case 0: 706 | _context6.next = 2; 707 | return client.exists(key); 708 | 709 | case 2: 710 | return _context6.abrupt('return', _context6.sent); 711 | 712 | case 3: 713 | case 'end': 714 | return _context6.stop(); 715 | } 716 | } 717 | }, _callee6, this); 718 | })); 719 | 720 | case 8: 721 | return _context8.abrupt('return', _context8.sent); 722 | 723 | case 11: 724 | _context8.prev = 11; 725 | _context8.t0 = _context8['catch'](5); 726 | 727 | // under some condition, it may not support exists command, wield 728 | debug('Error when trying invoke "exists" of redis driver:', _context8.t0); 729 | _context8.next = 16; 730 | return (0, _co2.default)(_regenerator2.default.mark(function _callee7() { 731 | return _regenerator2.default.wrap(function _callee7$(_context7) { 732 | while (1) { 733 | switch (_context7.prev = _context7.next) { 734 | case 0: 735 | _context7.next = 2; 736 | return client.get(key); 737 | 738 | case 2: 739 | return _context7.abrupt('return', _context7.sent); 740 | 741 | case 3: 742 | case 'end': 743 | return _context7.stop(); 744 | } 745 | } 746 | }, _callee7, this); 747 | })); 748 | 749 | case 16: 750 | value = _context8.sent; 751 | 752 | if (!value) { 753 | _context8.next = 21; 754 | break; 755 | } 756 | 757 | return _context8.abrupt('return', true); 758 | 759 | case 21: 760 | return _context8.abrupt('return', false); 761 | 762 | case 22: 763 | _context8.next = 25; 764 | break; 765 | 766 | case 24: 767 | return _context8.abrupt('return', exists); 768 | 769 | case 25: 770 | case 'end': 771 | return _context8.stop(); 772 | } 773 | } 774 | }, _callee8, this, [[5, 11]]); 775 | })); 776 | 777 | function exists(_x10) { 778 | return _ref6.apply(this, arguments); 779 | } 780 | 781 | return exists; 782 | }() 783 | }, { 784 | key: 'set', 785 | value: function () { 786 | var _ref7 = (0, _asyncToGenerator3.default)(_regenerator2.default.mark(function _callee9(key, value) { 787 | var storedValue; 788 | return _regenerator2.default.wrap(function _callee9$(_context9) { 789 | while (1) { 790 | switch (_context9.prev = _context9.next) { 791 | case 0: 792 | if (!(this.type === 'redis')) { 793 | _context9.next = 8; 794 | break; 795 | } 796 | 797 | if (!(!key || !this.client || !this.client.set)) { 798 | _context9.next = 3; 799 | break; 800 | } 801 | 802 | return _context9.abrupt('return'); 803 | 804 | case 3: 805 | storedValue = (typeof value === 'undefined' ? 'undefined' : (0, _typeof3.default)(value)) === 'object' ? (0, _stringify2.default)(value) : value; 806 | _context9.next = 6; 807 | return this.client.set(key, storedValue); 808 | 809 | case 6: 810 | _context9.next = 8; 811 | return this.client.ttl(key); 812 | 813 | case 8: 814 | case 'end': 815 | return _context9.stop(); 816 | } 817 | } 818 | }, _callee9, this); 819 | })); 820 | 821 | function set(_x11, _x12) { 822 | return _ref7.apply(this, arguments); 823 | } 824 | 825 | return set; 826 | }() 827 | }, { 828 | key: 'get', 829 | value: function () { 830 | var _ref8 = (0, _asyncToGenerator3.default)(_regenerator2.default.mark(function _callee11(key) { 831 | var client, value; 832 | return _regenerator2.default.wrap(function _callee11$(_context11) { 833 | while (1) { 834 | switch (_context11.prev = _context11.next) { 835 | case 0: 836 | if (!(this.type === 'redis')) { 837 | _context11.next = 12; 838 | break; 839 | } 840 | 841 | if (!(!key || !this.client || !this.client.get)) { 842 | _context11.next = 3; 843 | break; 844 | } 845 | 846 | return _context11.abrupt('return', null); 847 | 848 | case 3: 849 | client = this.client; 850 | _context11.next = 6; 851 | return (0, _co2.default)(_regenerator2.default.mark(function _callee10() { 852 | return _regenerator2.default.wrap(function _callee10$(_context10) { 853 | while (1) { 854 | switch (_context10.prev = _context10.next) { 855 | case 0: 856 | _context10.next = 2; 857 | return client.get(key); 858 | 859 | case 2: 860 | return _context10.abrupt('return', _context10.sent); 861 | 862 | case 3: 863 | case 'end': 864 | return _context10.stop(); 865 | } 866 | } 867 | }, _callee10, this); 868 | })); 869 | 870 | case 6: 871 | value = _context11.sent; 872 | 873 | if (!(value && typeof value === 'string')) { 874 | _context11.next = 11; 875 | break; 876 | } 877 | 878 | return _context11.abrupt('return', JSON.parse(value)); 879 | 880 | case 11: 881 | return _context11.abrupt('return', value); 882 | 883 | case 12: 884 | case 'end': 885 | return _context11.stop(); 886 | } 887 | } 888 | }, _callee11, this); 889 | })); 890 | 891 | function get(_x13) { 892 | return _ref8.apply(this, arguments); 893 | } 894 | 895 | return get; 896 | }() 897 | }]); 898 | return Store; 899 | }(); 900 | 901 | // Redis store 902 | 903 | 904 | var RedisStore = function (_Store) { 905 | (0, _inherits3.default)(RedisStore, _Store); 906 | 907 | function RedisStore(opts) { 908 | (0, _classCallCheck3.default)(this, RedisStore); 909 | 910 | var _this = (0, _possibleConstructorReturn3.default)(this, (RedisStore.__proto__ || (0, _getPrototypeOf2.default)(RedisStore)).call(this, opts)); 911 | 912 | _this.type = 'redis'; 913 | var redisOptions = opts || {}; 914 | debug('Redis options:', redisOptions); 915 | 916 | var db = redisOptions.db || 0; 917 | var ttl = _this.ttl = redisOptions.ttl || expiresIn || EXPIRES_IN_SECONDS; 918 | 919 | //redis client for session 920 | _this.client = new _ioredis2.default(redisOptions); 921 | 922 | var client = _this.client; 923 | 924 | client.select(db, function () { 925 | debug('redis changed to db %d', db); 926 | }); 927 | 928 | client.get = (0, _thunkify2.default)(client.get); 929 | client.exists = (0, _thunkify2.default)(client.exists); 930 | client.ttl = ttl ? function (key) { 931 | client.expire(key, ttl); 932 | } : function () {}; 933 | 934 | client.on('connect', function () { 935 | debug('redis is connecting'); 936 | }); 937 | 938 | client.on('ready', function () { 939 | debug('redis ready'); 940 | }); 941 | 942 | client.on('reconnect', function () { 943 | debug('redis is reconnecting'); 944 | }); 945 | 946 | client.on('error', function (err) { 947 | debug('redis encouters error: %j', err.stack || err); 948 | }); 949 | 950 | client.on('end', function () { 951 | debug('redis connection ended'); 952 | }); 953 | return _this; 954 | } 955 | 956 | return RedisStore; 957 | }(Store); -------------------------------------------------------------------------------- /lib/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _regenerator = require('babel-runtime/regenerator'); 4 | 5 | var _regenerator2 = _interopRequireDefault(_regenerator); 6 | 7 | var _asyncToGenerator2 = require('babel-runtime/helpers/asyncToGenerator'); 8 | 9 | var _asyncToGenerator3 = _interopRequireDefault(_asyncToGenerator2); 10 | 11 | require('babel-polyfill'); 12 | 13 | var _supertestKoaAgent = require('supertest-koa-agent'); 14 | 15 | var _supertestKoaAgent2 = _interopRequireDefault(_supertestKoaAgent); 16 | 17 | var _should = require('should'); 18 | 19 | var _should2 = _interopRequireDefault(_should); 20 | 21 | var _koa = require('koa'); 22 | 23 | var _koa2 = _interopRequireDefault(_koa); 24 | 25 | var _koaConvert = require('koa-convert'); 26 | 27 | var _koaConvert2 = _interopRequireDefault(_koaConvert); 28 | 29 | var _koaBodyparser = require('koa-bodyparser'); 30 | 31 | var _koaBodyparser2 = _interopRequireDefault(_koaBodyparser); 32 | 33 | var _index = require('./index.js'); 34 | 35 | var _index2 = _interopRequireDefault(_index); 36 | 37 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 38 | 39 | describe('Testing jwt-redis-session', function () { 40 | var _this = this; 41 | 42 | var app = new _koa2.default(); 43 | app.use((0, _koaBodyparser2.default)({ 44 | onerror: function onerror(err, ctx) { 45 | debug(DEBUG_LOG_HEADER, 'Body parser error:', err); 46 | } 47 | })); 48 | 49 | app.use((0, _index2.default)({ 50 | session: { 51 | sidKey: 'sid' 52 | } 53 | })); 54 | 55 | app.use(function () { 56 | var _ref = (0, _asyncToGenerator3.default)(_regenerator2.default.mark(function _callee(ctx, next) { 57 | return _regenerator2.default.wrap(function _callee$(_context) { 58 | while (1) { 59 | switch (_context.prev = _context.next) { 60 | case 0: 61 | if (!(ctx.path === '/test' && ctx.method.toUpperCase() === 'PUT')) { 62 | _context.next = 4; 63 | break; 64 | } 65 | 66 | if (ctx.session && ctx.session.sid) { 67 | ctx.body = { status: 'OK' }; 68 | } else { 69 | ctx.body = { status: 'ERROR' }; 70 | } 71 | _context.next = 6; 72 | break; 73 | 74 | case 4: 75 | _context.next = 6; 76 | return next(); 77 | 78 | case 6: 79 | case 'end': 80 | return _context.stop(); 81 | } 82 | } 83 | }, _callee, this); 84 | })); 85 | 86 | return function (_x, _x2) { 87 | return _ref.apply(this, arguments); 88 | }; 89 | }()); 90 | 91 | var token = void 0, 92 | ctxObj = void 0, 93 | userObj = void 0; 94 | 95 | it('Should generate token directly from createSession function', (0, _asyncToGenerator3.default)(_regenerator2.default.mark(function _callee2() { 96 | return _regenerator2.default.wrap(function _callee2$(_context2) { 97 | while (1) { 98 | switch (_context2.prev = _context2.next) { 99 | case 0: 100 | ctxObj = {}, userObj = { testAccount: 'testAccount111' }; 101 | _context2.next = 3; 102 | return (0, _index.createSession)(ctxObj, userObj); 103 | 104 | case 3: 105 | token = _context2.sent; 106 | 107 | token.should.have.property('token'); 108 | token.should.have.property('expiresIn'); 109 | ctxObj.session.should.have.property('testAccount'); 110 | ctxObj.session.testAccount.should.be.exactly('testAccount111'); 111 | ctxObj.session.should.have.property('_sessionId'); 112 | userObj.should.have.property('sid'); 113 | 114 | case 10: 115 | case 'end': 116 | return _context2.stop(); 117 | } 118 | } 119 | }, _callee2, _this); 120 | }))); 121 | 122 | it('Should authorise the token generated just now', (0, _asyncToGenerator3.default)(_regenerator2.default.mark(function _callee3() { 123 | return _regenerator2.default.wrap(function _callee3$(_context3) { 124 | while (1) { 125 | switch (_context3.prev = _context3.next) { 126 | case 0: 127 | ctxObj.header = { authorization: "Bearer " + token.token }; 128 | userObj = null; 129 | _context3.next = 4; 130 | return (0, _index.authoriseRequest)(ctxObj); 131 | 132 | case 4: 133 | userObj = _context3.sent; 134 | 135 | userObj.should.have.property('sid'); 136 | 137 | case 6: 138 | case 'end': 139 | return _context3.stop(); 140 | } 141 | } 142 | }, _callee3, _this); 143 | }))); 144 | 145 | token = null; 146 | it('Should get authorization token', function (done) { 147 | (0, _supertestKoaAgent2.default)(app).post('/authorize').send({ account: 'test', password: 'test' }).expect(200).expect(function (res) { 148 | token = res.body.token; 149 | res.body.should.have.property('token'); 150 | }).end(done); 151 | }); 152 | 153 | it('Access to protected resource with token should success', function (done) { 154 | (0, _supertestKoaAgent2.default)(app).put('/test').set('Authorization', 'Bearer ' + token).expect(200).expect(function (res) { 155 | res.body.should.have.property('status', 'OK'); 156 | }).end(done); 157 | }); 158 | }); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "bugs": { 3 | "url": "https://github.com/mrsunlin/koa-jwt-redis-session/issues" 4 | }, 5 | "dependencies": { 6 | "debug": "*", 7 | "redis": "^2.5.3", 8 | "thunkify": "^2.1.2", 9 | "uid2": "^0.0.3", 10 | "jsonwebtoken": "^5.7.0", 11 | "co": "^4.6.0", 12 | "ioredis": "^1.15.1" 13 | }, 14 | "description": "Pure JWT implementation using Redis as session storage for Koa 2, without any cookies", 15 | "devDependencies": { 16 | "koa": "^2.0.0", 17 | "koa-convert": "*", 18 | "koa-bodyparser": "^3.0.0", 19 | "mocha": "*", 20 | "should": "*", 21 | "supertest": "*", 22 | "supertest-koa-agent": "*", 23 | "babel-cli": "^6.6.5", 24 | "babel-core": "*", 25 | "babel-loader": "*", 26 | "babel-polyfill": "*", 27 | "babel-preset-es2015": "*", 28 | "babel-preset-es2016-node5": "*", 29 | "babel-plugin-transform-runtime": "*", 30 | "babel-plugin-transform-class-properties": "^6.6.0", 31 | "babel-preset-stage-0": "^6.22.0" 32 | }, 33 | "directories": {}, 34 | "files": [ 35 | "index.js", 36 | "lib", 37 | "src" 38 | ], 39 | "homepage": "https://github.com/mrsunlin/koa-jwt-redis-session", 40 | "keywords": [ 41 | "koa", 42 | "middleware", 43 | "jwt", 44 | "redis", 45 | "session", 46 | "non-cookie", 47 | "es6", 48 | "es7" 49 | ], 50 | "license": "MIT", 51 | "maintainers": [ 52 | { 53 | "name": "Lin Sun" 54 | } 55 | ], 56 | "name": "koa-jwt-redis-session", 57 | "optionalDependencies": {}, 58 | "repository": { 59 | "type": "git", 60 | "url": "git@github.com:lyalls/koa-jwt-redis-session.git" 61 | }, 62 | "scripts": { 63 | "test-dev": "mocha --compilers js:babel-core/register ./src/test.js", 64 | "test": "mocha ./lib/test.js", 65 | "build": "babel --presets es2015,es2016-node5 --plugins transform-class-properties,transform-runtime --out-dir ./lib ./src" 66 | }, 67 | "version": "0.0.30" 68 | } 69 | 70 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | let debug = require('debug')('koa-jwt-redis-session') 3 | 4 | import redis from 'ioredis' 5 | import JWT from 'jsonwebtoken' 6 | import thunkify from 'thunkify' 7 | import uid from 'uid2' 8 | import co from 'co' 9 | 10 | const DEBUG_LOG_HEADER = '[koa-jwt-redis-session]' 11 | const EXPIRES_IN_SECONDS = 60 * 60 12 | 13 | // Options 14 | // JWT Options 15 | let jwtOptions , contentType , charset, secret, authPath, registerPath, expiresIn, accountKey; 16 | let passwordKey, authHandler, registerHandler, jwtOpt, refreshTokenPath; 17 | // Session 18 | let sessionKey, sidKey, sessOpt; 19 | // Redis Options 20 | let redisOptions, redisStore, store; 21 | 22 | function parseOptions(opts) { 23 | // Options 24 | const options = opts || {} 25 | debug('Parsing options:', options); 26 | // JWT Options 27 | jwtOptions = options.jwt || {} 28 | contentType = jwtOptions.contentType || 'application/json' 29 | charset = jwtOptions.charset || 'utf-8' 30 | secret = jwtOptions.secret || 'koa-jwt-redis-session' + new Date().getTime() 31 | authPath = jwtOptions.authPath || '/authorize'; 32 | registerPath = jwtOptions.registerPath || '/register'; 33 | refreshTokenPath = jwtOptions.refreshTokenPath || '/refreshToken'; 34 | expiresIn = jwtOptions.expiresIn || EXPIRES_IN_SECONDS; 35 | accountKey = jwtOptions.accountKey || 'account'; 36 | passwordKey = jwtOptions.passwordKey || 'password'; 37 | authHandler = jwtOptions.authHandler || function (account, password) { 38 | if (account && password) { 39 | let user = {}; 40 | user[accountKey] = account; 41 | return user; 42 | } 43 | return false; 44 | } 45 | registerHandler = jwtOptions.registerHandler || function (account, password) { 46 | if (account && password) { 47 | let user = {}; 48 | user[accountKey] = account; 49 | return user; 50 | } 51 | return false; 52 | } 53 | jwtOpt = {expiresIn}; 54 | // Session 55 | let sessionOptions = options.session || {} 56 | sessionKey = sessionOptions.sessionKey || 'session'; 57 | sidKey = sessionOptions.sidKey || 'koa:sess'; 58 | sessOpt = {sidKey}; 59 | // Redis Options 60 | redisOptions = options.redis || {} 61 | redisStore = new RedisStore(redisOptions); 62 | store = redisStore; 63 | } 64 | 65 | let createSession = async (ctx, user)=>{ 66 | let sess = await Session.create(store, user, sessOpt); 67 | const token = await JWT.sign(user,secret,jwtOpt) 68 | ctx[sessionKey] = sess; 69 | debug('Generated token:', token) 70 | return {token, expiresIn}; 71 | } 72 | 73 | let authoriseRequest = async (ctx) => { 74 | //if(ctx.header.authorization){ 75 | let user = null; 76 | if(ctx && ctx.header && ctx.header.authorization) { 77 | const authComponents = ctx.header.authorization.split(' '); 78 | if (authComponents.length === 2 && authComponents[0] === 'Bearer') { 79 | try { 80 | user = await JWT.verify(authComponents[1], secret, jwtOpt) 81 | } catch (err) { 82 | debug('Illegal token'); 83 | } 84 | } 85 | } 86 | return user; 87 | } 88 | 89 | function middleware(opts) { 90 | parseOptions(opts); 91 | 92 | // Utilities 93 | function sendToken(ctx, token){ 94 | if(contentType.toLowerCase() === 'application/json') 95 | ctx.body = token; 96 | else ctx.body = token.token; 97 | } 98 | 99 | // Authorization by JWT 100 | return async function (ctx, next) { 101 | try { 102 | ctx.type = contentType + ';' + 'charset=' + charset; 103 | if (ctx.path === refreshTokenPath && ctx.method.toUpperCase() === 'POST' 104 | ) { 105 | let user = await authoriseRequest(ctx); 106 | if(user){ 107 | delete user.iat, user.exp; 108 | let token = await createSession(ctx, user); 109 | debug('Refreshed token:', token, 'user:', user) 110 | sendToken(ctx, token); 111 | }else{ 112 | // ctx.body= 'Authorization failed'; 113 | ctx.status = 401; 114 | } 115 | // SignIn 116 | }else if (ctx.path === authPath && ctx.method.toUpperCase() === 'POST' 117 | && ctx.request.body[accountKey] && ctx.request.body[passwordKey] 118 | ) { 119 | const account = ctx.request.body[accountKey]; 120 | const password = ctx.request.body[passwordKey]; 121 | debug('checking authorization:', account, password); 122 | let user = await authHandler(account, password); 123 | if( (typeof user === 'boolean' && user ) || 124 | Object.prototype.toString.call(user).toLowerCase() === "[object object]"){ 125 | let userObj; 126 | if(typeof user === 'boolean'){ 127 | userObj = {}; 128 | userObj[accountKey] = account; 129 | }else userObj = user; 130 | let token = await createSession(ctx, userObj) 131 | sendToken(ctx, token); 132 | }else{ 133 | // ctx.body= 'Authorization failed'; 134 | ctx.status = 401; 135 | } 136 | // Register 137 | } else if (ctx.path === registerPath && ctx.method.toUpperCase() === 'POST' 138 | && ctx.request.body[accountKey] && ctx.request.body[passwordKey] 139 | ) { 140 | const account = ctx.request.body[accountKey]; 141 | const password = ctx.request.body[passwordKey]; 142 | 143 | let user = await registerHandler(account, password); 144 | if( (typeof user === 'boolean' && user ) || 145 | Object.prototype.toString.call(user).toLowerCase() === "[object object]" 146 | ){ 147 | let userObj; 148 | if(typeof user === 'boolean'){ 149 | userObj = {}; 150 | userObj[accountKey] = account; 151 | }else userObj = user; 152 | let token = await createSession(ctx, userObj) 153 | sendToken(ctx, token); 154 | }else{ 155 | // ctx.body= 'Authorization failed'; 156 | ctx.status = 401; 157 | } 158 | }else { 159 | let user = await authoriseRequest(ctx) 160 | if(user){ 161 | debug('Authorized user:', user) 162 | ctx[sessionKey] = await Session.create(store, user, sessOpt); 163 | await next(); 164 | if(ctx[sessionKey] == undefined || ctx[sessionKey] === false){ 165 | // session is destroyed in the business 166 | }else{ 167 | await ctx[sessionKey].save(store); 168 | } 169 | } 170 | } 171 | } catch (ex) { 172 | if(ex.name !== 'UnauthorizedError' || ex.name !== 'JsonWebTokenError' ) { 173 | debug(DEBUG_LOG_HEADER, '[ERROR] catch something wrong:', ex) 174 | ctx.status = 500; 175 | if (ex.message && !ctx.body) ctx.body = ex.message; 176 | }else{ 177 | ctx.status = 401; 178 | } 179 | } 180 | }; 181 | } 182 | export default middleware; 183 | export {createSession, authoriseRequest}; 184 | 185 | // Session Model 186 | class Session { 187 | constructor (obj) { 188 | if (!obj) this.isNew = true; 189 | else for (var k in obj) this[k] = obj[k]; 190 | } 191 | 192 | /** 193 | * JSON representation of the session. 194 | * 195 | * @return {Object} 196 | * @api public 197 | */ 198 | get json() { 199 | var self = this; 200 | var obj = {}; 201 | 202 | Object.keys(this).forEach(function (key) { 203 | if ('isNew' === key) return; 204 | if ('_' === key[0]) return; 205 | obj[key] = self[key]; 206 | }); 207 | 208 | return obj; 209 | } 210 | 211 | get string () { 212 | return this._json || JSON.stringify(this.json) 213 | } 214 | 215 | /** 216 | * Check if the session has changed relative to the `prev` 217 | * JSON value from the request. 218 | * 219 | * @param {String} [prev] 220 | * @return {Boolean} 221 | * @api private 222 | */ 223 | changed (prev) { 224 | if (!prev) return true; 225 | this._json = JSON.stringify(this); 226 | return this._json !== prev; 227 | } 228 | 229 | /** 230 | * Return how many values there are in the session object. 231 | * Used to see if it's "populated". 232 | * 233 | * @return {Number} 234 | * @api public 235 | */ 236 | get length (){ 237 | return Object.keys(this.toJSON()).length; 238 | } 239 | 240 | 241 | /** 242 | * populated flag, which is just a boolean alias of .length. 243 | * 244 | * @return {Boolean} 245 | * @api public 246 | */ 247 | get populated (){ 248 | return !!this.length; 249 | } 250 | 251 | static generateSessionId (header){ 252 | if(!header){ 253 | return uid(24); 254 | }else{ 255 | return header+":"+uid(24); 256 | } 257 | } 258 | 259 | /** 260 | * Create a session instance 261 | * @param store 262 | * @param user 263 | */ 264 | static async create(store, user, opts){ 265 | let instance = user || {}; 266 | let options = opts || { 267 | sidKey: 'sid' 268 | }; 269 | debug('User for creating session:', instance); 270 | debug('Session options for creating session:', opts); 271 | if(!instance[options.sidKey]) { 272 | debug('Creating session'); 273 | // Creating 274 | let sid = Session.generateSessionId(options.sidKey); 275 | while (await store.exists(sid)){ 276 | debug('sid', sid, 'exists'); 277 | sid = Session.generateSessionId(options.sidKey); 278 | } 279 | debug('new sid:', sid); 280 | user[options.sidKey] = sid; 281 | instance[options.sidKey] = sid; 282 | let session = new Session(instance); 283 | session._sessionId = sid; 284 | await session.save(store); 285 | return session; 286 | }else{ 287 | debug('Loading session, sid:',instance[options.sidKey]) 288 | // loading 289 | instance = await store.get(instance[options.sidKey]); 290 | instance._sessionId = instance[options.sidKey]; 291 | let session = new Session(instance); 292 | debug('loaded session:', session.json) 293 | return session; 294 | } 295 | } 296 | 297 | async save(store) { 298 | if(!store) return; 299 | if(store.type === 'redis'){ 300 | await store.set(this._sessionId, this.json); 301 | } 302 | } 303 | } 304 | 305 | // Store Base Class 306 | class Store { 307 | constructor(opts){ 308 | } 309 | async exists(key){ 310 | let exists = true; 311 | if(this.type === 'redis') { 312 | if (!key || !this.client || !this.client.exists) return exists; 313 | const client = this.client; 314 | try { 315 | return await co(function*() { 316 | return yield client.exists(key); 317 | }) 318 | }catch (ex){ 319 | // under some condition, it may not support exists command, wield 320 | debug('Error when trying invoke "exists" of redis driver:', ex); 321 | let value = await co(function*(){ 322 | return yield client.get(key); 323 | }) 324 | if(value) return true; 325 | else return false; 326 | } 327 | }else{ 328 | return exists; 329 | } 330 | } 331 | 332 | async set(key, value){ 333 | if(this.type === 'redis'){ 334 | if(!key || !this.client || !this.client.set) return; 335 | let storedValue = (typeof value === 'object') ? JSON.stringify(value): value; 336 | await this.client.set(key, storedValue); 337 | await this.client.ttl(key) 338 | } 339 | } 340 | 341 | async get(key){ 342 | if(this.type === 'redis'){ 343 | if(!key || !this.client || !this.client.get) return null; 344 | const client = this.client; 345 | let value = await co(function*(){ 346 | return yield client.get(key); 347 | }) 348 | if(value && typeof value === 'string') return JSON.parse(value); 349 | else return value; 350 | } 351 | } 352 | } 353 | 354 | // Redis store 355 | class RedisStore extends Store{ 356 | constructor (opts){ 357 | super(opts) 358 | this.type = 'redis' 359 | let redisOptions = opts || {} 360 | debug('Redis options:', redisOptions); 361 | 362 | const db = redisOptions.db || 0 363 | const ttl = this.ttl = redisOptions.ttl || expiresIn || EXPIRES_IN_SECONDS 364 | 365 | //redis client for session 366 | this.client = new redis(redisOptions) 367 | 368 | const client = this.client; 369 | 370 | client.select(db, function () { 371 | debug('redis changed to db %d', db); 372 | }); 373 | 374 | client.get = thunkify(client.get); 375 | client.exists = thunkify(client.exists); 376 | client.ttl = ttl ? (key)=>{ client.expire(key, ttl); } : ()=>{}; 377 | 378 | client.on('connect', function () { 379 | debug('redis is connecting'); 380 | }); 381 | 382 | client.on('ready', function () { 383 | debug('redis ready'); 384 | }); 385 | 386 | client.on('reconnect', function () { 387 | debug('redis is reconnecting'); 388 | }); 389 | 390 | client.on('error', function (err) { 391 | debug('redis encouters error: %j', err.stack || err); 392 | }); 393 | 394 | client.on('end', function () { 395 | debug('redis connection ended'); 396 | }); 397 | } 398 | } 399 | -------------------------------------------------------------------------------- /src/test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | import 'babel-polyfill' 3 | import request from 'supertest-koa-agent' 4 | import should from 'should' 5 | import koa from 'koa' 6 | import convert from 'koa-convert' 7 | import bodyParser from 'koa-bodyparser' 8 | import session from './index.js' 9 | import {createSession, authoriseRequest} from './index.js' 10 | 11 | describe('Testing jwt-redis-session', function(){ 12 | const app = new koa(); 13 | app.use(bodyParser({ 14 | onerror: function(err, ctx){ 15 | debug(DEBUG_LOG_HEADER, 'Body parser error:', err) 16 | } 17 | })); 18 | 19 | app.use(session({ 20 | session: { 21 | sidKey: 'sid' 22 | } 23 | })); 24 | 25 | app.use(async function(ctx, next){ 26 | if(ctx.path === '/test' && ctx.method.toUpperCase() === 'PUT'){ 27 | if(ctx.session && ctx.session.sid) { 28 | ctx.body = {status:'OK'}; 29 | } else { 30 | ctx.body = {status:'ERROR'}; 31 | } 32 | }else{ 33 | await next(); 34 | } 35 | }); 36 | 37 | let token, ctxObj, userObj; 38 | 39 | it('Should generate token directly from createSession function', async ()=>{ 40 | ctxObj = {}, userObj = {testAccount: 'testAccount111'}; 41 | token = await createSession(ctxObj,userObj); 42 | token.should.have.property('token'); 43 | token.should.have.property('expiresIn'); 44 | ctxObj.session.should.have.property('testAccount'); 45 | ctxObj.session.testAccount.should.be.exactly('testAccount111'); 46 | ctxObj.session.should.have.property('_sessionId'); 47 | userObj.should.have.property('sid'); 48 | }); 49 | 50 | it('Should authorise the token generated just now', async ()=>{ 51 | ctxObj.header = {authorization: "Bearer " + token.token} 52 | userObj = null; 53 | userObj = await authoriseRequest(ctxObj); 54 | userObj.should.have.property('sid'); 55 | }); 56 | 57 | token = null; 58 | it('Should get authorization token', function(done){ 59 | request(app).post('/authorize') 60 | .send({account: 'test', password: 'test'}) 61 | .expect(200) 62 | .expect(function(res){ 63 | token = res.body.token; 64 | res.body.should.have.property('token') 65 | }) 66 | .end(done) 67 | }); 68 | 69 | it('Access to protected resource with token should success', function (done) { 70 | request(app).put('/test') 71 | .set('Authorization', 'Bearer ' + token) 72 | .expect(200) 73 | .expect(function(res){ 74 | res.body.should.have.property('status', 'OK'); 75 | }) 76 | .end(done) 77 | }); 78 | }) 79 | --------------------------------------------------------------------------------