├── .travis.yml ├── LICENSE ├── README.md ├── index.js ├── package.json └── test.js /.travis.yml: -------------------------------------------------------------------------------- 1 | notifications: 2 | email: false 3 | language: node_js 4 | node_js: 5 | - node 6 | deploy: 7 | provider: npm 8 | email: 9 | secure: UrH0MKd0uflwlEn8gVuqcpc2ayj3qP/W3/yyJfzpJpCyp2ON/bP7NC+CEPpZZ+LI5D1iWFSQ2T7YDMjHn9nxMwErHUPbrxKzWaA3evPy6Z5mVuBh5FmKEafEIzsMUwp2KHnAH1c3cg5+6cUFWZMmFC38jcQjuDb8hR1gWbK2E1IiCod8xlGfzqP+83pOmCTgdeyegIUpODFp7n0laBl4nRfI/9kFjaIpyvqVYGQj+Rg68WzEP//EYigaQEcnIQon8mBituc30N4H6zWvB0YUHiYch17uargBN/c5GgClSkSNwMxvg3wknJgmydAv+7sUlw0RW0XVob+sFnfstsQOqtHO5IpfC3c7KfXVWW8uW3EiruPAb8i9zlZao5e7DqogbIOM4HSlnnpJCGjkeRku8DAPZmQhHcRvEQolTclXQd8beXVew94NjIb6P5bXTLZR/R15VEGC6kkC/DSStLBsAuhYZuGvBsYVt/P9RNumCChPFz7thB5aNFrBbtQwQxzfSTedcQ0jHYj1jLZmMkuY21YFpyWrr5qhPGTT4MBD4EPRBaadhrwwB73E0oqzXNv+fCkGFDe5HApm5Z8r+lydcM3Xz5+7AFmjOEVGBilbYMLw1SDYOdRl0xV4e4q+GZiMSkvspXvInPeUb50EJolsdIKvGARsaved6L2yuiGPM8c= 10 | api_key: 11 | secure: rrepAcWYKkfKAiwDnToBUcYgdUGcAFvzhP6NuYUhbmboAtvaLK1vmcXdtT+UAv+JV2rRK3IcwfP5SPioq5PEuKeyXMnIOqUk/lYRPfQfmfuLY/hirerea+4o1ifHj1N5MFAS10xa69S93Bg4t+8YVlDqyiUzBATdIETP8sIR8v8Ap9y2SVrAv8pKz3WCZWikBOUpsEQ9ivrNlNwgbRyXourCp7vbdlf+gIIvhKr1rmKstug1YVB5BtfVyxyjtEhrm+HLcCj5pIDLx/CJuR10mxoJ8bcJyxgmuEm12jvmik+SFi/lhYQFIxoIwg0x5qf/k+oIP2dBOx5As8JPiG/ehiBOhJoMJLCtL7F69P2kq+RIl9fq1xFms7wNLJ48kbXkbcWO2tWbRrolurlZyXm+MjFt3XmGUEAN/++8hJ9U6rzuHm+1m2cwZ/5A8bgnG60BwmsKNSasrorbhPPgJQhQBY01ArlIsJasz8PmiLErpZ2Xv759nvKcK2SMnHeNqjNFtwaaTRBZmwquINc0NqQ6zss7WrNxY/eEPjBcd2Kab62NtwevSpvyY4g6Pk75Wh4R/zqCjpGWMEKLBH0VrsdsB44ojalx3vrrTJ0PcAp8HVXZsLN2xLi2JLZmyVowyGhSuKavhNUjbopc/7TzFzu16v63+WYwggmh37NbzFiprRE= 12 | on: 13 | tags: true 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017, Emil Bay 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `secure-token` 2 | 3 | [![Build Status](https://travis-ci.org/emilbayes/secure-token.svg?branch=master)](https://travis-ci.org/emilbayes/secure-token) 4 | 5 | > Simple, secure tokens for authentication, access keys, sessions etc. 6 | 7 | ## Usage 8 | 9 | Below is an example using `secure-token` stored in cookies. Note that you use 10 | `secureToken.hash` to store and verify a token: 11 | 12 | ```js 13 | var secureToken = require('secure-token') 14 | var db = new Map() // Use map as database for simplicity 15 | 16 | function login (req, res) { 17 | // Do authentication 18 | // ... 19 | // If success issue a session token 20 | var sessionToken = secureToken.create() 21 | 22 | // Here we use the 'session' 23 | db.set(secureToken.hash(sessionToken, 'session'), true) 24 | 25 | res.writeHead(204, { 26 | 'Set-Cookie': [ 27 | `sessionToken=${secureToken.toString('base64')}`, 28 | 'HttpOnly', 29 | 'Secure' 30 | ].join(';') 31 | }) 32 | res.end() 33 | } 34 | 35 | function secretPage (req, res) { 36 | // Get req.sessionToken somehow 37 | var sessionToken = Buffer.from(req.sessionToken, 'base64') 38 | 39 | var hash = secureToken.hash(sessionToken, 'session') 40 | 41 | if (!db.get(hash)) { 42 | res.writeHead(400) 43 | return res.end() 44 | } 45 | 46 | res.writeHead(200) 47 | return res.end('Yay!') 48 | } 49 | ``` 50 | 51 | ## API 52 | 53 | ### `var tokenBuf = secureToken.create([size])` 54 | 55 | Create a new token from your OS Cryptographically Secure Pseudorandom Number 56 | Generator (CSPRNG), making the token unpredictable and return as a `Buffer`. 57 | `size` defaults to 18, giving a security level of more than 128 bits, while 58 | avoiding any padding when Base 64 encoded. 59 | 60 | ### `var hashBuf = secureToken.hash(tokenBuf, [namespace])` 61 | 62 | Hash a token for long-term storage, taking `Buffer` `tokenBuf` and an optional 63 | `namespace` which can be either a string or `Buffer`. You can use `namespace` to 64 | partition your tokens for different use-cases, invalidating tokens which are 65 | used for the wrong purpose, while keeping the information hidden in storage. 66 | `namespace` does not add any significant security and is simply so that 67 | different tokens are not used in the wrong context. 68 | 69 | `tokenBuf` should be a token generated by `secureToken.create` and namespace 70 | can be a `Buffer` or `String`. 71 | 72 | The reason it is important to obscure the token is that it is password 73 | equivalent, meaning having access to a valid token is the same as having gone 74 | through an authentication process, eg. typing a password. You do not want anyone 75 | with access to your tokens to be able to impersonate a user. 76 | 77 | Using the default token size it should take well over 2^64 guesses to find two 78 | tokens that yield the same hash value due to the birthday paradox. 79 | 80 | 81 | ## Install 82 | 83 | ```sh 84 | npm install secure-token 85 | ``` 86 | 87 | ## License 88 | 89 | [ISC](LICENSE.md) 90 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var sodium = require('sodium-native') 2 | var assert = require('nanoassert') 3 | 4 | var APPTOKEN_BYTES_MIN = 16 // 128 bits 5 | var APPTOKEN_BYTES = 18 // fits into base64 encoding without padding 6 | 7 | // Why 18? Because 18 bytes > 128 bits, making an adversary do at least 2^64 8 | // attempts before finding a hash collision, ie. being able to fake a token 9 | // 18 is the first number after 16 (128 bits) where `x mod 6 = 0`, 10 | // meaning it base64 encodes without padding 11 | function create (size) { 12 | assert(size == null ? true : size >= APPTOKEN_BYTES_MIN, 'size must be at least APPTOKEN_BYTES_MIN (' + APPTOKEN_BYTES_MIN + ')') 13 | assert(size == null ? true : Number.isSafeInteger(size), 'size must be safe integer') 14 | 15 | var res = Buffer.alloc(size || APPTOKEN_BYTES) 16 | sodium.randombytes_buf(res) 17 | return res 18 | } 19 | 20 | var EMPTY_BUF = Buffer.alloc(0) 21 | // namespace can be used to seperate different various token uses eg. session, 22 | // access, deploy etc. 23 | function hash (tokenBuf, namespace) { 24 | assert(Buffer.isBuffer(tokenBuf), 'tokenBuf must be Buffer') 25 | 26 | if (namespace == null) namespace = EMPTY_BUF 27 | if (typeof namespace === 'string') namespace = Buffer.from(namespace) 28 | assert(namespace == null ? true : Buffer.isBuffer(namespace), 'namespace must be Buffer') 29 | 30 | var output = Buffer.alloc(sodium.crypto_generichash_BYTES) 31 | 32 | sodium.crypto_generichash(output, Buffer.concat([namespace, tokenBuf])) 33 | return output 34 | } 35 | 36 | module.exports = { 37 | create: create, 38 | hash: hash, 39 | APPTOKEN_BYTES_MIN: APPTOKEN_BYTES_MIN, 40 | APPTOKEN_BYTES: APPTOKEN_BYTES 41 | } 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "secure-token", 3 | "version": "1.1.0", 4 | "description": "Simple, secure tokens for authentication, access keys, sessions etc.", 5 | "main": "index.js", 6 | "dependencies": { 7 | "nanoassert": "^2.0.0", 8 | "sodium-native": "^3.1.1" 9 | }, 10 | "devDependencies": { 11 | "tape": "^4.8.0" 12 | }, 13 | "scripts": { 14 | "test": "tape test.js" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/emilbayes/secure-token.git" 19 | }, 20 | "keywords": [ 21 | "token", 22 | "access", 23 | "session", 24 | "auth", 25 | "authentication" 26 | ], 27 | "author": "Emil Bay ", 28 | "license": "ISC", 29 | "bugs": { 30 | "url": "https://github.com/emilbayes/secure-token/issues" 31 | }, 32 | "homepage": "https://github.com/emilbayes/secure-token#readme" 33 | } 34 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | var secureToken = require('.') 3 | 4 | test('', function (assert) { 5 | var token = secureToken.create() 6 | 7 | assert.notOk(secureToken.hash(token).equals(token)) 8 | assert.ok(secureToken.hash(token).equals(secureToken.hash(token))) 9 | assert.notOk(secureToken.hash(token, 'session').equals(secureToken.hash(token))) 10 | assert.end() 11 | }) 12 | --------------------------------------------------------------------------------