├── .gitignore ├── .gitpod.yml ├── .jshintrc ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── Procfile ├── README-zh_CN.md ├── README.md ├── example ├── lib │ └── helpers.js ├── server.js ├── test │ ├── functional.js │ ├── integration.js │ └── mock.js └── views │ ├── fail.html │ ├── index.html │ └── restricted.html └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (https://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Commenting this out is preferred by some people, see 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 25 | node_modules 26 | 27 | # Users Environment Variables 28 | .lock-wscript 29 | example/lib/db 30 | package-lock.json 31 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | ports: 2 | - port: 1337 3 | onOpen: open-preview 4 | tasks: 5 | - init: npm install 6 | command: node example/server.js 7 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "asi" : true, 3 | "laxbreak" : true, 4 | "bitwise" : true, 5 | "boss" : false, 6 | "curly" : true, 7 | "eqeqeq" : true, 8 | "eqnull" : false, 9 | "evil" : false, 10 | "expr" : false, 11 | "forin" : false, 12 | "immed" : true, 13 | "indent" : 2, 14 | "latedef" : true, 15 | "loopfunc" : false, 16 | "noarg" : true, 17 | "node" : true, 18 | "regexp" : true, 19 | "regexdash" : false, 20 | "strict" : false, 21 | "scripturl" : true, 22 | "shadow" : false, 23 | "supernew" : false, 24 | "sub" : true, 25 | "undef" : true, 26 | "white" : true 27 | } 28 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "node" 4 | after_success: 5 | - bash <(curl -s https://codecov.io/bash) 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | _**Please read** our_ 2 | [**contribution guide**](https://github.com/dwyl/contributing) 3 | (_thank you_!) 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2025 Do What You Love (DWYL) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: node example/server.js 2 | -------------------------------------------------------------------------------- /README-zh_CN.md: -------------------------------------------------------------------------------- 1 |  2 | 3 | # 学习如何使用 **JSON Web Tokens** (JWT) 进行**鉴权** 4 | 5 |  6 | 7 | 学习怎么使用 JSON Web Token (JWT) 来**加密**你的 Web 应用或者移动应用! 8 | 9 | [](https://travis-ci.org/dwyl/learn-json-web-tokens) 10 | [](https://codecov.io/github/dwyl/learn-json-web-tokens?branch=master) 11 | [](https://codeclimate.com/github/dwyl/learn-json-web-tokens/maintainability) 12 | [](https://david-dm.org/dwyl/learn-json-web-tokens) 13 | [](https://david-dm.org/dwyl/learn-json-web-tokens?type=dev) 14 | [](https://github.com/dwyl/learn-json-web-tokens/issues) 15 | [](https://hits.dwyl.io/dwyl/learn-json-web-tokens) 16 | 17 | 18 | ## **为什么**? 19 | 20 | JSON Web Tokens (JWTs) 使得在服务之间(**包括在你 app 或者网站的内部和外部**) _**发送只读签名**_ 的 “_**声明**_“ 变得很**简单**。 21 | 22 | 声明是你想让某些人**阅读**或**校验**但不能修改的**任意**字节的数据。 23 | 24 | > **注意**:**如果听起来很啰嗦,请不要担心,阅读 5 分钟之后一切都会变得清晰起来的!** 25 | 26 | ## 是什么? 27 | 28 | > “***JSON Web Token***(JWT)是一种紧凑的 URL 安全方式,用于表示在双方之间传输的声明。JWT 中的声明被**编码**为使用JSON Web 签名(JWS)进行数字签名的 **JSON 对象**。——IETF 29 | 30 | ### **通俗一点** 31 | 32 | 为了在你的 app(web或者移动端)中辨识或授权用户,在 **header** 或者页面(或者 API)的 **url** 中放置一个**基于标准的 token**,它表明了这个用户已经登录并且被允许获取到他想要的内容。 33 | 34 | 示例:`https://www.yoursite.com/private-content/?token=eyJ0eXAiOiJKV1Qi.eyJrZXkiOi.eUiabuiKv` 35 | 36 | > **注意**:如果这对于你而言还不够“安全”,往下翻到“[***security***](#q-我把-jwt-放在-url-或者-header-是安全的吗)”这一节。 37 | 38 | ### JWT **看起来**是什么样的? 39 | 40 | Tokens 是一系列“url 安全”的字符所组成的字符串,它包含了**编码**后的信息。 41 | Tokens 由**三部分**组成(用小数点分割),为了便于阅读,下面用三行来展示,但是实际使用时是一个单独的字符串。 42 | 43 | ``` 44 | eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9 // 头部 45 | .eyJrZXkiOiJ2YWwiLCJpYXQiOjE0MjI2MDU0NDV9 // 载荷 46 | .eUiabuiKv-8PYk2AkGY4Fb5KMZeorYBLw261JPQD5lM // 签名 47 | ``` 48 | 49 | #### 1. 头部 50 | 51 | JWT 的第一部分是一个编码的字符串,它表示一个简单的 JavaScript 对象,这个对象描述了 token 所使用的哈希算法。 52 | 53 | #### 2. 载荷 54 | 55 | JWT 的第二部分是 token 的核心,负载的长度与你在 JWT 中所存储的数据长度有关。 56 | 通常所遵守的准则是:存储尽量少的必要的数据在 JWT 中。 57 | 58 | 59 | #### 3. 签名 60 | 61 | 第三部分是最后一部分,是根据头部(第一部分)和主体(第二部分)所计算出来的一个签名,会被用于**校验** JWT 是否有效。 62 | 63 | ### 什么是“声明”? 64 | 65 | Claims are the predefined **keys** and their **values**: 66 | 67 | 声明是预定义的一系列**键**和它们所对应的**值**: 68 | 69 | + **iss**: token 的发行人。 70 | + **exp**: 到期时间戳(已过期的令牌会被拒绝)。注意:如规范中所定义,以秒为单位。 71 | + **iat**: token 的发行时间。可以用于判断 JWT 的发行时间长。 72 | + **nbf**: "not before" 是 JWT 被激活的某个未来时间(可以理解为生效时间)。 73 | + **jti**: JWT 的第一无二的标识(编号),用于防止 JWT 被重复使用或者重复产生。 74 | + **sub**: token 的主题(很少使用)。 75 | + **aud**: token 的受众(同样很少使用)。 76 | 77 | 详情阅读: https://self-issued.info/docs/draft-ietf-oauth-json-web-token.html#RegisteredClaimName 78 | 79 | # 示例 [](https://github.com/dwyl/learn-json-web-tokens/issues) 80 | 81 | 让我们通过一个简单的示例来继续深入学习 JWT。 82 | (**全部**源码都在 **/example** 目录下) 83 | 84 | > 动手尝试: https://jwt.herokuapp.com/ 85 | 86 | 可以在 Gitpod(需要通过 GitHub 授权登录) 上亲自动手尝试一下这个示例。 87 | 88 | [](https://gitpod.io#https://github.com/dwyl/learn-json-web-tokens/blob/master/example/lib/helpers.js) 89 | 90 | ## 服务器 91 | 92 | 通过使用 **node.js 的 核心模块 http** 服务器,我们在 **/example/server.js** 创建了四个接口: 93 | 94 | 1. **/home** : 首页(不是必要的,但是我们的 **login** 表单放在这里)。 95 | 2. **/auth** : 对游客进行**授权** (如果授权失败会返回错误并且回到首页的 login 表单)。 96 | 3. **/private** : 我们的受保护的内容 - ***需要登录***(有合法的会话 token)才能看到这个页面。 97 | 4. **/logout** : 使 token 失效并且登出用户(防止重复使用旧的 token)。 98 | 99 | 我们已经**有意地**把 **server.js** 写得足够的简单了: 100 | 101 | + 可阅读 102 | + 可维护 103 | + 可测试(所有的 helper/handler 方法都已经被单独测试) 104 | 105 | > 注意: 如果你可以让示例更**简单**,请提交 [issue](https://github.com/dwyl/learn-json-web-tokens/issues) 一起讨论! 106 | 107 | ## Helper 方法 108 | 109 | 所有 helper 类方法都保存在 **/example/lib/helpers.js** 110 | 两个最有意思或者说最相关的方法是(下面是简化版本): 111 | 112 | ```javascript 113 | // 构造 JWT 的方法。 114 | function generateToken(req){ 115 | return jwt.sign({ 116 | auth: 'magic', 117 | agent: req.headers['user-agent'], 118 | exp: Math.floor(new Date().getTime()/1000) + 7*24*60*60; // 注意:单位是秒! 119 | }, secret); // secret 被定义在环境变量 JWT_SECRET 中 120 | } 121 | ``` 122 | 123 | 当用户进行授权的时候,这个方法会计算出我们的 JWT token,这个 token 随后会被放在 **Authorization** 响应头发送回客户端,用于后续的请求。 124 | 125 | 另外一个 126 | 127 | ```javascript 128 | // 校验请求头 Authorization 中的 token 是否有效。 129 | function validate(req, res) { 130 | var token = req.headers.authorization; 131 | try { 132 | var decoded = jwt.verify(token, secret); 133 | } catch (e) { 134 | return authFail(res); 135 | } 136 | if(!decoded || decoded.auth !== 'magic') { 137 | return authFail(res); 138 | } else { 139 | return privado(res, token); 140 | } 141 | } 142 | ``` 143 | 144 | Which **checks the JWT supplied by the client is valid**, 145 | shows private ("privado") content to the requestor if valid 146 | and renders the **authFail** ***error*** page if its not. 147 | 148 | 该方法**会检查客户端提供的 JWT 是否有效**,如果有效就会展示私有内容(通过 "privado" 方法)给请求者;如果校验失败,会通过**authFail** 方法渲染 ***错误页*** 。 149 | 150 | **注意**: 这两个方法**都是同步的**。这两个方法都没有进行任何的 IO 操作或者网络请求,所以同步计算是安全的。 151 | 152 | > 提示:如果你正在为你的 Hapi.js 应用寻找 ***全能的*** **JWT Auth Hapi.js 插件** (**异步**校验或验证) 请查看: [https://github.com/**dwyl/hapi-auth-jwt2**](https://github.com/dwyl/hapi-auth-jwt2) 153 | 154 | ## 测试 155 | 156 | 你可能已经注意到了教程开头的 [![Build Status][travis-image]][travis-url] 这些铭牌,这是一个标志,作者不只是**堆砌**代码在一起。 157 | 158 | 对服务器路由和 helper 类方法的测试都放在 **/example/test**。 159 | 160 | 1. /example/test/**functional.js** - 测试我们在 /example/lib/**helpers.js** 中创建的所有 **helper 类方法**。 161 | [](https://codeclimate.com/github/dwyl/learn-json-web-tokens) 162 | 2. /example/test/**integration.js** - 模拟**用户**对服务器所发起的请求并测试服务器**响应**。 163 | 164 | 请**阅读**所有测试案例,如有不清楚的地方,可以**告诉我们**。 165 | 166 | **注意**:我们为 http req/res 对象写了一个基本的“**mock**”: /example/test/**mock.js** 167 | 168 | 如果还不懂 mock 或者很好奇,请阅读:[When to Mock (by "Uncle Bob")](https://blog.8thlight.com/uncle-bob/2014/05/10/WhenToMock.html) 169 | 170 | - - - 171 | 172 | ## 常见问题及解答 173 | 174 | > ***有问题吗? 马上提问!*** >> https://github.com/dwyl/learn-json-web-tokens/issues 175 | 176 | ### Q: 我把 JWT 放在 *URL* 或者 *Header* 是**安全**的吗? 177 | 178 | 问得好!答案是:“**否**”,除非你使用 SSL/TLS 加密你的连接(https),使用[明文](https://en.wikipedia.org/wiki/Plaintext)发送 Token 永远都是不安全的(token 可以被拦截并且被坏蛋重用)。一种比较笨拙简单的方法是添加校验声明到 token,比如检查请求是否来自于同一个浏览器(user-agent),添加IP 地址或者更先进的“[**browser fingerprints**](https://stackoverflow.com/a/3287761/1148249)”…… https://programmers.stackexchange.com/a/122385 179 | 180 | 解决方案包括: 181 | + 使用一次性 token,在链接点击后即失效 ***或者*** 182 | + 在安全性要求较高的场景下不把 token 放在 url 中。 183 | (比如:不把执行交易的链接发送给别人) 184 | 185 | JWT 放在 url 中的**使用场景**: 186 | + 账户校验 - 当你把激活账户的链接通过 Email 发送给在你网站注册了的客户的时候。 `https://yoursite.co/account/verify?token=jwt.goes.here` 187 | + 密码重置 - 确保重置密码的人能够登录与账户有关的邮件。 188 | `https://yoursite.co/account/reset-password?token=jwt.goes.here` 189 | 190 | 上面的案例都是使用一次性 token 的适用场景 (****点击后就失效****)。 191 | 192 | ### Q: 怎么使会话失效? 193 | 194 | 如果使用你 app 的人的**设备**(手机/平板电脑/笔记本电脑)**被盗了**,那你应该如何使它们使用的 token 失效? 195 | 196 | JWT 背后的思想是**无状态**,它们可以被集群中的任意节点计算出来并且验证,而不用对数据库发起任何请求。 197 | 198 | #### 把 token 存在数据库中? 199 | 200 | ##### LevelDB 201 | 202 | 如果你的应用规模比较**小**,或者你不想运行一个 Redis 服务器,你可以通过使用 LevelDB:http://leveldb.org/ 来从 Redis 获取最大的好处。 203 | 204 | 我们可以把有效的 token 存储在数据库中,或者相反地把非法的 token 存储在数据库中。这两种方案都需要往返数据库以检查 token 是否有效。所以我们倾向于存储所有的 token 到数据库,并且把 token 的 *valid* 字段从 true 更新为 false,表示 token 已经过期。 205 | 206 | 存储在 LevelDB 中的示例: 207 | ```json 208 | "GUID" : { 209 | "auth" : "true", 210 | "created" : "timestamp", 211 | "uid" : "1234" 212 | } 213 | ``` 214 | 我们将通过 GUID 来查找这条记录: 215 | 216 | ```js 217 | var db = require('level'); 218 | db.get(GUID, function(err, record){ 219 | // pseudo-code 220 | if(record.auth){ 221 | // 展示私有内容(通过了校验) 222 | } else { 223 | // 展示错误信息(校验未通过) 224 | } 225 | }); 226 | ``` 227 | 通过查看 example/lib/helpers.js 中的 **validate** 方法获取更多详情。 228 | 229 | ##### Redis 230 | 231 | Redis 是存储令牌的**可扩展**方式。 232 | 233 | 如果你**从未**接触过 Redis,请阅读: 234 | + Intro: https://redis.io/topics/introduction 235 | + Redis in 30 mins: 236 | https://openmymind.net/2011/11/8/Redis-Zero-To-Master-In-30-Minutes-Part-1/ 237 | + What is Redis? https://www.slideshare.net/dvirsky/introduction-to-redis 238 | 239 | Redis ***Scales*** (provided you have the RAM): 240 | https://stackoverflow.com/questions/10478794/more-than-4-billion-key-value-pairs-in-redis 241 | 242 | > ***从现在开始学习 Redis!*** [https://github.com/dwyl/**learn-redis**](https://github.com/dwyl/learn-redis) 243 | 244 | #### Memcache? 245 | 246 | ***Quick* answer**: *使用 **Redis***: 247 | https://stackoverflow.com/questions/10558465/memcache-vs-redis 248 | 249 | 250 | ### Q: 返回游客(**会话之间没有状态保存**) 251 | 252 | Cookie 存储在客户端,并且每次请求时都会由浏览器发送到服务器。如果关闭了浏览器,则会保留 Cookie,因此可以在上次停止的地方继续操作而不必再次登录。但是 cookie 会在与路径和发布域匹配的所有请求上发送,包括不需要的图像和 css 请求。 253 | 254 | [`localStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window.localStorage) 提供了一种更好的在浏览器会话之间存储 token 的机制。 255 | 256 | #### 基于浏览器的应用 257 | 258 | 有以下两种方式存储你的 JWT: 259 | 260 | 1. 使用 ***localStorage*** 在客户端存储你的 JWT(**意味着你需要把 JWT 放在 `authorization` header 中返回给客户端,以便后续 http/ajax 请求使用**) 261 | 2. 把 JWT 存储在 cookie 中。 262 | 263 | > 我们更倾向于第一种方法。但是如果使用得当的话,cookie 仍然可以在现代 web 应用中发挥他们的作用! 264 | 265 | ##### 一些有用的网站 266 | 267 | + Good ***history*** & overview of **Localstorage**: 268 | http://diveintohtml5.info/storage.html 269 | + MDN **Window.localStorage**: 270 | https://developer.mozilla.org/en-US/docs/Web/API/Window.localStorage 271 | + Brief description + basic *examples*: 272 | https://www.html5rocks.com/en/features/storage 273 | + Will it work for *my* visitors? 274 | https://caniuse.com/#search=localstorage 275 | (**Quick answer**: ***Yes***! IE 8 & above, Android 4.0+, IOS 7.1+, Chrome & Firefox ) 276 | 277 | 278 | #### 编程式(API)访问 279 | 280 | 其它服务访问你的 API 时必须把令牌存储在检索系统中(比如:移动应用的 Redis 或 SQLite)并且把 token 带入到每一个请求中。 281 | 282 | ### 如何生成密钥? 283 | 284 | > “**如果这个问题在其它地方被提到过的话我感到抱歉。用于计算 token 的私钥和 ssh-keygen 生成的私钥是一样的吗?** ~最初由 [@skota](https://github.com/skota) 提出问题,更多详细: [dwyl/**hapi-auth-jwt2/issues**/48](https://github.com/dwyl/hapi-auth-jwt2/issues/48) 285 | 286 | 因为 JSON Web Token(JWT)不要求使用[**非对称加密**](https://en.wikipedia.org/wiki/Public-key_cryptography)进行签名,所以**不必**使用 ssh-keygen 生成密钥。你可以简单地只使用一个**强密码**,例如:https://www.grc.com/passwords.htm 提供了足够长的复杂的随机的字符串。这样的话使用相同加密字符串的可能性(有人能够修改有效负载,添加或修改声明以及创建有效签名的可能性)非常低。如果你将两个**强密码**(字符串)连接在一起,你将拥有一个 128 位的 ASCII 字符串。因此,碰撞的可能性小于[宇宙中的原子数](https://en.wikipedia.org/wiki/Observable_universe#Matter_content_.E2.80.94_number_of_atoms)。 287 | 288 | To quickly and easily create a secret key using Node's crypto library, run this command. 289 | 290 | 使用以下命令可以通过 Node 的 crypto 模块快速而简单地创建一个密钥。 291 | 292 | node -e "console.log(require('crypto').randomBytes(32).toString('hex'));" 293 | 294 | 换句话说,你**可以**使用一个 ***RSA 密钥***,但是这不是必要的。 295 | 296 | 你需要记住的最重要的一件事就是:不要把这个密钥泄露给核心组(”*DevOps Team*“)成员之外的任何人或者**意外地**将它发布到了 GitHub! 297 | 298 | 299 | ## 哪个 Node.js 模块? 300 | 301 | 在 NPM 上搜索 ”**JSON Web Token**“:https://www.npmjs.com/search?q=json+web+token 会产生许多结果! 302 | 303 |  304 | 305 | ### 使用 Hapi.js 构建 Web 应用? 306 | 307 | 我们努力简化在 Hapi.js 应用程序中使用 JWT 的过程,在这个过程中我们写了这个模块:https://github.com/dwyl/hapi-auth-jwt2 308 | 309 | 310 | ### **其它** Node.js 项目的常用方法 311 | 312 | 我们**强烈**推荐 **jsonwebtoken** 这个模块,它由我们的朋友[@auth0](https://twitter.com/auth0) 313 | ([校验/鉴权领域的专家](https://auth0.com/about))编写: 314 | - https://github.com/auth0/node-jsonwebtoken 315 | Which in turn uses: 316 | https://github.com/brianloveswords/node-jws 317 | [![NPM][jsonwebtoken-icon] ][jsonwebtoken-url] 318 | 319 | 另外一个非常棒的选择是: https://github.com/joaquimserafim/json-web-token 320 | 也是我们的朋友 [@joaquimserafim](https://github.com/joaquimserafim) 编写的。 321 | 322 | ## 必要的阅读(**预习**) 323 | 324 | - Original **Specification** (Draft): 325 | https://tools.ietf.org/html/draft-ietf-oauth-json-web-token-32 326 | - Great overview from Atlassian: 327 | https://developer.atlassian.com/cloud/jira/platform/understanding-jwt/ 328 | - Good intro (ruby-specific examples): 329 | https://www.intridea.com/blog/2013/11/7/json-web-token-the-useful-little-standard-you-haven-t-heard-about 330 | + Friendlier introduction: https://jwt.io/ 331 | + Getting to know JWT: 332 | https://scotch.io/tutorials/the-anatomy-of-a-json-web-token 333 | - Discussion: https://ask.auth0.com/c/jwt 334 | + ***How to*** do **stateless authentication** (session-less & cookie-less): 335 | https://stackoverflow.com/questions/20588467/how-to-do-stateless-session-less-cookie-less-authentication 336 | 337 | 338 | ## 深入阅读(**推荐**) [](https://github.com/dwyl/learn-json-web-tokens/issues) 339 | 340 | + JWT with Passport.js: 341 | https://stackoverflow.com/questions/20228572/passport-local-with-node-jwt-simple 342 | + JWT Tokens as API Keys: 343 | https://auth0.com/blog/2014/12/02/using-json-web-tokens-as-api-keys/ 344 | + **10 Things you should know** about ***Tokens and Cookies***: 345 | https://auth0.com/blog/2014/01/27/ten-things-you-should-know-about-tokens-and-cookies/#xss-xsrf 346 | + Information Security discussion: 347 | https://security.stackexchange.com/questions/51294/json-web-tokens-jwt-as-user-identification-and-authentication-tokens 348 | + Using JWT with node.js (express + backbone): 349 | https://www.sitepoint.com/using-json-web-tokens-node-js/ 350 | + Token-based Authentication with Socket.IO 351 | https://auth0.com/blog/2014/01/15/auth-with-socket-io/ 352 | + JWT Auth *discussion* on Hacker News: 353 | https://news.ycombinator.com/item?id=7084435 354 | + The Spec but nicer: 355 | https://self-issued.info/docs/draft-ietf-oauth-json-web-token.html 356 | + Extended (Wiki) article on Claims-based authentication: 357 | https://en.wikipedia.org/wiki/Claims-based_identity 358 | + Securing Requests with JWT: 359 | https://websec.io/2014/08/04/Securing-Requests-with-JWT.html 360 | + Avoid Database in authenticating user for each request (stateless): 361 | https://security.stackexchange.com/questions/49145/avoid-hitting-db-to-authenticate-a-user-on-every-request-in-stateless-web-app-ar 362 | + The Twelve-Factor App: https://12factor.net/ + https://12factor.net/processes 363 | + Auth in Hapi with JWT: https://medium.com/@thedon/auth-in-hapi-with-jwt-780ce4d072c7#.clgj5lknq 364 | + Token based authentication in Node.js with Passport, JWT and bcrypt: https://jonathas.com/token-based-authentication-in-nodejs-with-passport-jwt-and-bcrypt/ 365 | 366 | # **感谢**您和我们一起学习! 367 | 368 | 如果您认为这篇快速阅读很有帮助, 请在 GitHub 上给我们一颗星星(Star)并且转推分享给其他人:https://twitter.com/olizilla/status/626487231860080640 369 | 370 | [](https://twitter.com/olizilla/status/626487231860080640 "Please Re-Tweet!") 371 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |  2 | 3 | # Learn how to use *JSON Web Tokens* (JWT) for *Authentication* 4 | 5 |  6 | 7 | Learn how to use JSON Web Token (JWT) to *secure* your Web and/or Mobile Application! 8 | 9 | [](https://travis-ci.org/dwyl/learn-json-web-tokens) 10 | [](https://codecov.io/github/dwyl/learn-json-web-tokens?branch=master) 11 | [](https://codeclimate.com/github/dwyl/learn-json-web-tokens/maintainability) 12 | [](https://david-dm.org/dwyl/learn-json-web-tokens) 13 | [](https://david-dm.org/dwyl/learn-json-web-tokens?type=dev) 14 | [](https://github.com/dwyl/learn-json-web-tokens/issues) 15 | [](https://hits.dwyl.io/dwyl/learn-json-web-tokens) 16 | 17 | 18 | ## *Why*? 19 | 20 | JSON Web Tokens (JWTs) make it *easy* to _**send read-only signed**_ "_**claims**_" 21 | between services (*both internal and external to your app/site*). 22 | Claims are *any* bits of data that you want someone else to be able to *read* 23 | and/or *verify* but ***not alter***. 24 | 25 | 26 | > **Note**: *If that sounds buzz-wordy, don't worry, it will all become clear in the next 5 mins of reading!* 27 | 28 | ## What? 29 | 30 | > "***JSON Web Token*** *(JWT) is a compact* ***URL-safe*** *means of 31 | > representing claims to be transferred between two parties. 32 | > The claims in a JWT are* ***encoded*** *as a* ***JSON object*** *that is* ***digitally 33 | > signed*** *using JSON Web Signature (JWS)*. ~ IETF 34 | 35 | ### In *English* 36 | 37 | To identify/authenticate people in your (web/mobile) app, 38 | put a ***standards-based token*** in the **header** or **url** of the page 39 | (or API endpoint) which proves the user has logged in and is allowed to 40 | access the desired content. 41 | 42 | example: `https://www.yoursite.com/private-content/?token=eyJ0eXAiOiJKV1Qi.eyJrZXkiOi.eUiabuiKv` 43 | 44 | > **Note**: if this does not *look* "secure" to you, 45 | scroll down to the "[***security***](https://github.com/dwyl/learn-json-web-tokens#q-if-i-put-the-jwt-in-the-url-or-header-is-it-secure)" section. 46 | 47 | ### What does a JWT *Look* Like? 48 | 49 | Tokens are a string of "url safe" characters which *encode* information. 50 | Tokens have **three components** (separated by periods) 51 | (shown here on multiple lines for *readability* but used as a single string of text) 52 | 53 | ``` 54 | eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9 // header 55 | .eyJrZXkiOiJ2YWwiLCJpYXQiOjE0MjI2MDU0NDV9 // payload 56 | .eUiabuiKv-8PYk2AkGY4Fb5KMZeorYBLw261JPQD5lM // signature 57 | ``` 58 | 59 | #### 1. Header 60 | 61 | The first part of a JWT is an encoded string representation 62 | of a simple JavaScript object which describes the token along with the hashing algorithm used. 63 | 64 | #### 2. Payload 65 | 66 | The second part of the JWT forms the core of the token. 67 | Payload length is proportional to the amount of data you store in the JWT. 68 | General rule of thumb is: store the bare minimum in the JWT. 69 | 70 | 71 | #### 3. Signature 72 | 73 | The third, and final, part of the JWT is a signature generated 74 | based on the header (part one) and the body (part two) and will be used 75 | to *verify* that the JWT is valid. 76 | 77 | ### What are "Claims"? 78 | 79 | Claims are the predefined **keys** and their **values**: 80 | 81 | + **iss**: issuer of the token 82 | + **exp**: the expiration timestamp (reject tokens which have expired). Note: as defined in the spec, this must be in seconds. 83 | + **iat**: The time the JWT was issued. Can be used to determine the age of the JWT 84 | + **nbf**: "not before" is a future time when the token will become active. 85 | + **jti**: unique identifier for the JWT. Used to prevent the JWT from being re-used or replayed. 86 | + **sub**: subject of the token (rarely used) 87 | + **aud**: audience of the token (also rarely used) 88 | 89 | See: https://self-issued.info/docs/draft-ietf-oauth-json-web-token.html#RegisteredClaimName 90 | 91 | # Example [](https://github.com/dwyl/learn-json-web-tokens/issues) 92 | 93 | Lets get stuck in with a simple example. 94 | (the *full* source is in the **/example** directory) 95 | 96 | > TRY it: https://jwt.herokuapp.com/ 97 | 98 | To play around with the example you can open it in Gitpod (requires OAuth with GitHub). 99 | 100 | [](https://gitpod.io#https://github.com/dwyl/learn-json-web-tokens/blob/master/example/lib/helpers.js) 101 | 102 | ## Server 103 | 104 | Using the *core* **node.js http** server we create 4 endpoints in **/example/server.js**: 105 | 106 | 1. **/home** : home page (not essential but its where our **login** form is.) 107 | 2. **/auth** : *authenticate* the visitor (returns error + login form if failed) 108 | 3. **/private** : our restricted content - ***login required*** (valid session token) to see this page 109 | 4. **/logout** : invalidates the token and logs out the user (prevent from re-using old token) 110 | 111 | We have *deliberately* made **server.js** as _simple as possible_ for: 112 | 113 | + Readability 114 | + Maintainability 115 | + Testability (all helper/handler methods are tested separately) 116 | 117 | > note: if you can make it _simpler_, please submit an [issue](https://github.com/dwyl/learn-json-web-tokens/issues) to discuss! 118 | 119 | ## Helper Methods 120 | 121 | All the helper methods are kept in **/example/lib/helpers.js** 122 | The two most interesting/relevant methods are (simplified versions shown here): 123 | 124 | ```javascript 125 | // generate the JWT 126 | function generateToken(req){ 127 | return jwt.sign({ 128 | auth: 'magic', 129 | agent: req.headers['user-agent'], 130 | exp: Math.floor(new Date().getTime()/1000) + 7*24*60*60; // Note: in seconds! 131 | }, secret); // secret is defined in the environment variable JWT_SECRET 132 | } 133 | ``` 134 | Which ***generates*** our JWT token when the user authenticates (this is then sent back to the client in the **Authorization** header for use in subsequent requests), 135 | 136 | and 137 | 138 | ```javascript 139 | // validate the token supplied in request header 140 | function validate(req, res) { 141 | var token = req.headers.authorization; 142 | try { 143 | var decoded = jwt.verify(token, secret); 144 | } catch (e) { 145 | return authFail(res); 146 | } 147 | if(!decoded || decoded.auth !== 'magic') { 148 | return authFail(res); 149 | } else { 150 | return privado(res, token); 151 | } 152 | } 153 | ``` 154 | 155 | Which **checks the JWT supplied by the client is valid**, 156 | shows private ("privado") content to the requestor if valid 157 | and renders the **authFail** ***error*** page if its not. 158 | 159 | **Note**: *Yes*, *both* these methods are ***synchronous***. 160 | But, given that neither of these methods require *any* **I/O** *or* **Network** requests, 161 | its pretty safe to compute them synchronously. 162 | 163 | > Tip: If you're looking for a ***Full Featured*** **JWT Auth Hapi.js plugin** (which does the verification/validation *asynchronously*) for your Hapi.js-based app please check out: [https://github.com/**dwyl/hapi-auth-jwt2**](https://github.com/dwyl/hapi-auth-jwt2) 164 | 165 | ## Tests 166 | 167 | You may have noticed the [![Build Status][travis-image]][travis-url] badge at the *start* of this tutorial. 168 | This is a sign the author(s) are not just *cobbling* code together. 169 | The tests for both the server routes and helper functions are in: **/example/test** 170 | 171 | 1. /example/test/**functional.js** - *exercises* all the **helper methods** we created in /example/lib/**helpers.js** 172 | [](https://codeclimate.com/github/dwyl/learn-json-web-tokens) 173 | 2. /example/test/**integration.js** - simulates the requests a *user* would send to the server and tests the *responses*. 174 | 175 | Please *read* through the tests and *tell us* if anything is unclear! 176 | **Note**: We wrote a basic "***mock***" of the http req/res objects see: /example/test/**mock.js** 177 | Confused/curious about Mocking? Read [When to Mock (by "Uncle Bob")](https://blog.8thlight.com/uncle-bob/2014/05/10/WhenToMock.html) 178 | 179 | - - - 180 | 181 | ## Frequently Asked Questions (*FAQ*) 182 | 183 | > ***Got a Question? Ask!*** >> https://github.com/dwyl/learn-json-web-tokens/issues 184 | 185 | 186 | ### Q: If I put the JWT in the *URL* or *Header* is it *secure*? 187 | 188 | Good question! The *quick* **answer** is: ***No***. 189 | Unless you are using SSL/TLS (http**s** in your url) to encrypt the connection, 190 | sending the Token [***in-the-clear***](https://en.wikipedia.org/wiki/Plaintext) 191 | is *always* going to be insecure (the token can be intercepted and re-used by a bad person...). 192 | A *naive* "*mitigation*" is to add *verifiable* "claims" to the token 193 | such as checking that the request came from the ***same browser*** (user-agent), 194 | **IP address** or more advanced 195 | "[**browser fingerprints**](https://stackoverflow.com/a/3287761/1148249)" 196 | ... https://programmers.stackexchange.com/a/122385 197 | 198 | The solution is to *either*: 199 | + use one-time-use (_single use_) tokens (_which expire after the link has been clicked_) ***or*** 200 | + Don't use url-tokens where high degree of security is required. 201 | (e.g: don't send someone a link which allows them to perform a transaction) 202 | 203 | **Use-cases** for a JWT token in a url are: 204 | + account verification - when you email a person a link after they register on your site. `https://yoursite.co/account/verify?token=jwt.goes.here` 205 | + password re-set - ensures that the person re-setting the password has access to the email belonging to the account. 206 | `https://yoursite.co/account/reset-password?token=jwt.goes.here` 207 | 208 | Both of these are good candidates for single-use tokens (_which expire after they have been clicked_). 209 | 210 | ### Q: How do we *Invalidate* sessions? 211 | 212 | The person using your app has their **device** (phone/tablet/laptop) 213 | ***stolen***. How do you invalidate the token they were using? 214 | 215 | The idea behind JWT is that the tokens are ***stateless*** 216 | they can be **computed** by any node in a cluster and verified 217 | without a (slow) request to a database. 218 | 219 | #### Store the Token in a Database? 220 | 221 | ##### LevelDB 222 | 223 | If your app is *small* or you don't want to have to run a Redis server, 224 | you can get most of the benefits of Redis by using LevelDB: http://leveldb.org/ 225 | 226 | We can ***either*** store the ***valid*** Tokens in the DB **or** 227 | we can store the ***invalid*** tokens. 228 | Both of these require a round-trip to the DB to check if valid/invalid. 229 | So we prefer to store ***all*** tokens and update the 230 | **valid** property of the token from true to false. 231 | 232 | Example record stored in LevelDB 233 | ```json 234 | "GUID" : { 235 | "auth" : "true", 236 | "created" : "timestamp", 237 | "uid" : "1234" 238 | } 239 | ``` 240 | We would lookup this record by its GUID: 241 | 242 | ```js 243 | var db = require('level'); 244 | db.get(GUID, function(err, record){ 245 | // pseudo-code 246 | if(record.auth){ 247 | // display private content 248 | } else { 249 | // show error message 250 | } 251 | }); 252 | ``` 253 | see: example/lib/helpers.js **validate** method for detail. 254 | 255 | ##### Redis 256 | 257 | Redis is the *scalable* way of storing your tokens. 258 | 259 | If you are *totally* new to Redis read: 260 | + Intro: https://redis.io/topics/introduction 261 | + Redis in 30 mins: 262 | https://openmymind.net/2011/11/8/Redis-Zero-To-Master-In-30-Minutes-Part-1/ 263 | + What is Redis? https://www.slideshare.net/dvirsky/introduction-to-redis 264 | 265 | Redis ***Scales*** (provided you have the RAM): 266 | https://stackoverflow.com/questions/10478794/more-than-4-billion-key-value-pairs-in-redis 267 | 268 | > ***Get Started with Redis today***! [https://github.com/dwyl/**learn-redis**](https://github.com/dwyl/learn-redis) 269 | 270 | #### Memcache? 271 | 272 | ***Quick* answer**: *use **Redis***: 273 | https://stackoverflow.com/questions/10558465/memcache-vs-redis 274 | 275 | 276 | ### Q: Returning Visitor (*no State Preservation between sessions*) 277 | 278 | Cookies are stored on the client and are sent by the browser to the server on every request. If the person *closes* their browser, cookies are preserved, so they can continue where they left off without having to log-in again. However, cookies will be sent on **all** requests that match the path and issuing domain, including those for images and css, where it isn't needed. 279 | 280 | [`localStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window.localStorage) provides a better mechanism for storing tokens during and between browser sessions. 281 | 282 | #### Browser-based Applications 283 | 284 | There are two options for storing your JWTs: 285 | 1. Use ***localStorage*** to store your JWTs on the client side (_means you need to remember to send the JWT in your `authorization` header for subsequent http/ajax requests_) 286 | 2. Store your JWT in a cookie (_set and forget_) 287 | 288 | > We _obviously_ prefer the cookie-less approach. 289 | But if done right, cookies still have their place in modern web apps! 290 | (_see the Auth0 article on "10 things you should know" in the further reading below_) 291 | 292 | ##### Useful Links 293 | 294 | + Good ***history*** & overview of **Localstorage**: 295 | http://diveintohtml5.info/storage.html 296 | + MDN **Window.localStorage**: 297 | https://developer.mozilla.org/en-US/docs/Web/API/Window.localStorage 298 | + Brief description + basic *examples*: 299 | https://www.html5rocks.com/en/features/storage 300 | + Will it work for *my* visitors? 301 | https://caniuse.com/#search=localstorage 302 | (**Quick answer**: ***Yes***! IE 8 & above, Android 4.0+, IOS 7.1+, Chrome & Firefox ) 303 | 304 | 305 | #### Programmatic (API) Access 306 | 307 | Other services accessing your API will have to store the token in a 308 | retrieval system (e.g: Redis or SQLite for mobile apps) and send the token back on each request. 309 | 310 | ### How to generate secret key? 311 | 312 | > "*Apologies if this is mentioned elsewhere. The private key used for signing the tokens, is this the same as a private key generated using ssh-keygen?*" ~ Originally asked by [@skota](https://github.com/skota) see: [dwyl/**hapi-auth-jwt2/issues**/48](https://github.com/dwyl/hapi-auth-jwt2/issues/48) 313 | 314 | 315 | Since JSON Web Tokens (JWT) do not have to be signed using [***asymmetric encryption***](https://en.wikipedia.org/wiki/Public-key_cryptography) you do not *have* to generate your secret key using ***ssh-keygen***. You can just as easily use a ***strong password*** e.g: https://www.grc.com/passwords.htm provided it's ***long and random***. The chance of collision (and thus someone being able to modify the payload, adding or modifying claims, and create a valid signature) is pretty low. And if you join two of those **Strong Passwords** (*strings*) together, you'll have a 128bit ASCII String. So the chances of collision are less than the [number of *atoms* in the universe](https://en.wikipedia.org/wiki/Observable_universe#Matter_content_.E2.80.94_number_of_atoms). 316 | 317 | To quickly and easily create a secret key using Node's crypto library, run this command. 318 | 319 | node -e "console.log(require('crypto').randomBytes(32).toString('hex'));" 320 | 321 | In other words, you *can* use an ***RSA key***, but you don't *have to*. 322 | 323 | The main thing you need to remember is: don't share the key with people who are not in your core ("*DevOps Team*") or *accidentally* publish it by committing it to GitHub! 324 | 325 | 326 | 327 | 328 | ## Which Node.js Module? 329 | 330 | A search for "**JSON Web Token**" on NPM: 331 | https://www.npmjs.com/search?q=json+web+token yields ***many*** results! 332 | 333 |  334 | 335 | ### Building a Web App with Hapi.js? 336 | 337 | In our efforts to simplify using JWTs in Hapi.js apps, 338 | we wrote this module: https://github.com/dwyl/hapi-auth-jwt2 339 | 340 | 341 | ### General Use in *Other* Node.js Projects 342 | 343 | We *highly* recommend using the **jsonwebtoken** module 344 | made by our friends [@auth0](https://twitter.com/auth0) 345 | ([the identity/authentication experts](https://auth0.com/about)): 346 | - https://github.com/auth0/node-jsonwebtoken 347 | Which in turn uses: 348 | https://github.com/brianloveswords/node-jws 349 | [![NPM][jsonwebtoken-icon] ][jsonwebtoken-url] 350 | 351 | Another great option is: https://github.com/joaquimserafim/json-web-token 352 | by our friend [@joaquimserafim](https://github.com/joaquimserafim) 353 | 354 | ## Essential Reading (_Background_) 355 | 356 | - Original **Specification** (Draft): 357 | https://tools.ietf.org/html/draft-ietf-oauth-json-web-token-32 358 | - Great overview from Atlassian: 359 | https://developer.atlassian.com/cloud/jira/platform/understanding-jwt/ 360 | - Good intro (ruby-specific examples): 361 | http://www.intridea.com/blog/2013/11/7/json-web-token-the-useful-little-standard-you-haven-t-heard-about 362 | + Friendlier introduction: https://jwt.io/ 363 | + Getting to know JWT: 364 | https://scotch.io/tutorials/the-anatomy-of-a-json-web-token 365 | - Discussion: https://ask.auth0.com/c/jwt 366 | + ***How to*** do **stateless authentication** (session-less & cookie-less): 367 | https://stackoverflow.com/questions/20588467/how-to-do-stateless-session-less-cookie-less-authentication 368 | 369 | 370 | ## Further Reading (_Recommended_) [](https://github.com/dwyl/learn-json-web-tokens/issues) 371 | 372 | + JWT with Passport.js: 373 | https://stackoverflow.com/questions/20228572/passport-local-with-node-jwt-simple 374 | + JWT Tokens as API Keys: 375 | https://auth0.com/blog/2014/12/02/using-json-web-tokens-as-api-keys/ 376 | + **10 Things you should know** about ***Tokens and Cookies***: 377 | https://auth0.com/blog/2014/01/27/ten-things-you-should-know-about-tokens-and-cookies/#xss-xsrf 378 | + Information Security discussion: 379 | https://security.stackexchange.com/questions/51294/json-web-tokens-jwt-as-user-identification-and-authentication-tokens 380 | + Using JWT with node.js (express + backbone): 381 | https://www.sitepoint.com/using-json-web-tokens-node-js/ 382 | + Token-based Authentication with Socket.IO 383 | https://auth0.com/blog/2014/01/15/auth-with-socket-io/ 384 | + JWT Auth *discussion* on Hacker News: 385 | https://news.ycombinator.com/item?id=7084435 386 | + The Spec but nicer: 387 | https://self-issued.info/docs/draft-ietf-oauth-json-web-token.html 388 | + Extended (Wiki) article on Claims-based authentication: 389 | https://en.wikipedia.org/wiki/Claims-based_identity 390 | + Securing Requests with JWT: 391 | https://websec.io/2014/08/04/Securing-Requests-with-JWT.html 392 | + Avoid Database in authenticating user for each request (stateless): 393 | https://security.stackexchange.com/questions/49145/avoid-hitting-db-to-authenticate-a-user-on-every-request-in-stateless-web-app-ar 394 | + The Twelve-Factor App: https://12factor.net/ + https://12factor.net/processes 395 | + Auth in Hapi with JWT: https://medium.com/@thedon/auth-in-hapi-with-jwt-780ce4d072c7#.clgj5lknq 396 | + Token based authentication in Node.js with Passport, JWT and bcrypt: https://jonathas.com/token-based-authentication-in-nodejs-with-passport-jwt-and-bcrypt/ 397 | + JWT Signing Algorithms: https://www.loginradius.com/blog/async/jwt-signing-algorithms/ 398 | 399 | # *Thanks* for Learning with Us! 400 | 401 | If you found this quick guide useful, please star it on GitHub! 402 | and re-tweet to share it with others: https://twitter.com/olizilla/status/626487231860080640 403 | 404 | [](https://twitter.com/olizilla/status/626487231860080640 "Please Re-Tweet!") 405 | -------------------------------------------------------------------------------- /example/lib/helpers.js: -------------------------------------------------------------------------------- 1 | var qs = require('querystring'); 2 | var fs = require('fs'); 3 | var path = require('path'); 4 | 5 | var level = require('level'); 6 | var db = level(__dirname + '/db'); 7 | 8 | var jwt = require('jsonwebtoken'); 9 | var secret = process.env.JWT_SECRET || "CHANGE_THIS_TO_SOMETHING_RANDOM"; // super secret 10 | 11 | function loadView(view) { 12 | var filepath = path.resolve(__dirname, '../views/', view + '.html'); 13 | return fs.readFileSync(filepath).toString(); 14 | } 15 | 16 | // Content 17 | var index = loadView('index'); // default page 18 | var restricted = loadView('restricted'); // only show if JWT valid 19 | var fail = loadView('fail'); // auth fail 20 | 21 | // show fail page (login) 22 | function authFail(res, callback) { 23 | res.writeHead(401, {'content-type': 'text/html'}); 24 | return res.end(fail); 25 | } 26 | 27 | // generate a GUID 28 | function generateGUID() { 29 | return new Date().getTime(); // we can do better with crypto 30 | } 31 | 32 | // create JWT 33 | function generateToken(req, GUID, opts) { 34 | opts = opts || {}; 35 | 36 | // By default, expire the token after 7 days. 37 | // NOTE: the value for 'exp' needs to be in seconds since 38 | // the epoch as per the spec! 39 | var expiresDefault = '7d'; 40 | 41 | var token = jwt.sign({ 42 | auth: GUID, 43 | agent: req.headers['user-agent'] 44 | }, secret, { expiresIn: opts.expires || expiresDefault }); 45 | 46 | return token; 47 | } 48 | 49 | function generateAndStoreToken(req, opts) { 50 | var GUID = generateGUID(); // write/use a better GUID generator in practice 51 | var token = generateToken(req, GUID, opts); 52 | var record = { 53 | "valid" : true, 54 | "created" : new Date().getTime() 55 | }; 56 | 57 | db.put(GUID, JSON.stringify(record), function (err) { 58 | // console.log("record saved ", record); 59 | }); 60 | 61 | return token; 62 | } 63 | 64 | function authSuccess(req, res) { 65 | var token = generateAndStoreToken(req); 66 | 67 | res.writeHead(200, { 68 | 'content-type': 'text/html', 69 | 'authorization': token 70 | }); 71 | return res.end(restricted); 72 | } 73 | 74 | // lookup person in "database" 75 | var u = { un: 'masterbuilder', pw: 'itsnosecret' }; 76 | 77 | // handle authorisation requests 78 | function authHandler(req, res){ 79 | if (req.method === 'POST') { 80 | var body = ''; 81 | req.on('data', function (data) { 82 | body += data; 83 | }).on('end', function () { 84 | var post = qs.parse(body); 85 | if(post.username && post.username === u.un && post.password && post.password === u.pw) { 86 | return authSuccess(req, res); 87 | } else { 88 | return authFail(res); 89 | } 90 | }); 91 | } else { 92 | return authFail(res); 93 | } 94 | } 95 | 96 | function verify(token) { 97 | var decoded = false; 98 | try { 99 | decoded = jwt.verify(token, secret); 100 | } catch (e) { 101 | decoded = false; // still false 102 | } 103 | return decoded; 104 | } 105 | 106 | // can't use the word private as its an ES "future" reserved word! 107 | // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar#Keywords 108 | function privado(res, token) { 109 | res.writeHead(200, { 110 | 'content-type': 'text/html', 111 | 'authorization': token 112 | }); 113 | return res.end(restricted); 114 | } 115 | 116 | function validate(req, res, callback) { 117 | var token = req.headers.authorization; 118 | var decoded = verify(token); 119 | if(!decoded || !decoded.auth) { 120 | authFail(res); 121 | return callback(res); 122 | 123 | } else { 124 | // check if a key exists, else import word list: 125 | db.get(decoded.auth, function (err, record) { 126 | var r; 127 | try { 128 | r = JSON.parse(record); 129 | } catch (e) { 130 | r = { valid : false }; 131 | } 132 | if (err || !r.valid) { 133 | authFail(res); 134 | return callback(res); 135 | } else { 136 | privado(res, token); 137 | return callback(res); 138 | } 139 | }); 140 | } 141 | } 142 | 143 | function exit(res) { 144 | res.writeHead(404, {'content-type': 'text/plain'}); 145 | res.end('bye'); 146 | process.exit(); // kill the server! 147 | } 148 | 149 | function notFound(res) { 150 | res.writeHead(404, {'content-type': 'text/plain'}); 151 | return res.end('Not Found'); 152 | } 153 | 154 | function home(res) { 155 | res.writeHead(200, {'content-type': 'text/html'}); 156 | return res.end(index); 157 | } 158 | 159 | function done(res) { 160 | return; // does nothing. (pass as callback) 161 | } 162 | 163 | function logout(req, res, callback) { 164 | // invalidate the token 165 | var token = req.headers.authorization; 166 | // console.log(' >>> ', token) 167 | var decoded = verify(token); 168 | if(decoded) { // otherwise someone can force the server to crash by sending a bad token! 169 | // asynchronously read and invalidate 170 | db.get(decoded.auth, function(err, record){ 171 | var updated = JSON.parse(record); 172 | updated.valid = false; 173 | db.put(decoded.auth, updated, function (err) { 174 | // console.log('updated: ', updated) 175 | res.writeHead(200, {'content-type': 'text/plain'}); 176 | res.end('Logged Out!'); 177 | return callback(res); 178 | }); 179 | }); 180 | } else { 181 | authFail(res, done); 182 | return callback(res); 183 | } 184 | } 185 | 186 | 187 | module.exports = { 188 | fail : authFail, 189 | exit: exit, 190 | done: done, // moch callback 191 | home: home, 192 | handler : authHandler, 193 | logout : logout, 194 | notFound : notFound, 195 | success : authSuccess, 196 | validate : validate, 197 | verify : verify, 198 | view : loadView, 199 | generateAndStoreToken: generateAndStoreToken 200 | } 201 | -------------------------------------------------------------------------------- /example/server.js: -------------------------------------------------------------------------------- 1 | var port = process.env.PORT || 1337; // let heroku define port or use 1337 2 | var http = require('http'); // core node.js http (no frameworks) 3 | var url = require('url'); // core node.js url (no frameworks) 4 | var app = require('./lib/helpers'); // auth, token verification & render helpers 5 | var c = function(res){ /* */ }; 6 | 7 | http.createServer(function (req, res) { 8 | var path = url.parse(req.url).pathname; 9 | if( path === '/' || path === '/home' ) { app.home(res); } // homepage 10 | else if( path === '/auth') { app.handler(req, res); } // authenticator 11 | else if( path === '/private') { app.validate(req, res, app.done); } // private content 12 | else if( path === '/logout') { app.logout(req, res, app.done); } // end session 13 | else if( path === '/exit') { app.exit(res); } // for testing ONLY 14 | else { app.notFound(res); } // 404 error 15 | }).listen(port); 16 | 17 | console.log("Visit: http://127.0.0.1:" + port); 18 | -------------------------------------------------------------------------------- /example/test/functional.js: -------------------------------------------------------------------------------- 1 | var test = require('tape'); 2 | var qs = require('querystring'); 3 | 4 | var lib = require('../lib/helpers'); // auth, token verification & render helpers 5 | var mock = require('./mock'); // basic mock of http module req & res 6 | var token = null; // starts out empty 7 | // views 8 | var index = lib.view('index'); // default page 9 | var success = lib.view('restricted'); // only show if JWT valid 10 | var fail = lib.view('fail'); // auth fail 11 | 12 | var jwt = require('jsonwebtoken'); 13 | var secret = "CHANGE_THIS_TO_SOMETHING_RANDOM"; // super secret 14 | 15 | 16 | test("home", function (t) { 17 | var res = lib.home(mock.res); 18 | t.equal(200, 200, "Status 200"); 19 | t.equal(res.body.toString(), index, "Homepage rendered" ) 20 | t.end(); 21 | }); 22 | 23 | test("fail", function (t) { 24 | var res = lib.fail(mock.res); 25 | // console.log(res.status); 26 | t.equal(res.status, 401, "Status 401"); 27 | t.equal(res.body.toString(), fail, "Rejected (as expected)" ) 28 | t.end(); 29 | }); 30 | 31 | test("success", function (t) { 32 | var res = lib.success(mock.req, mock.res); 33 | // console.log(res); 34 | t.equal(res.status, 200, "Successfully authenticated"); 35 | t.equal(res.body.toString(), success, "Success."); 36 | t.end(); 37 | }); 38 | 39 | test("handler incorrect username & password", function (t) { 40 | var res = lib.handler(mock.req, mock.res); 41 | var postdata = { username: 'badguy', password: 'kragle' } 42 | mock.req.emit('data', qs.stringify(postdata)); 43 | mock.req.emit('end'); 44 | t.equal(mock.res.status, 401, "Auth fail"); 45 | t.end(); 46 | }); 47 | 48 | test("handler GET", function (t) { 49 | mock.req.method = 'GET'; 50 | var res = lib.handler(mock.req, mock.res); 51 | t.equal(res.status, 401, "GET should fail"); 52 | t.end(); 53 | }); 54 | 55 | test("handler", function (t) { 56 | mock.req.method = 'POST'; 57 | var res = lib.handler(mock.req, mock.res); 58 | var postdata = { username: 'masterbuilder', password: 'itsnosecret' } 59 | mock.req.emit('data', qs.stringify(postdata)); 60 | mock.req.emit('end'); 61 | token = mock.res.headers.authorization; 62 | // console.log(lib.verify(token)); 63 | t.equal(mock.res.status, 200, "Authenticated"); 64 | t.end(); 65 | }); 66 | 67 | test("validation fail (bad-but-valid token)", function (t) { 68 | var token = jwt.sign({ 69 | auth: 'invalid', 70 | agent: mock.req.headers['user-agent'] 71 | }, secret, { expiresIn: '7d' }); 72 | 73 | // console.log(lib.verify(token)); 74 | 75 | mock.req.headers.authorization = token; // we got this above 76 | lib.validate(mock.req, mock.res, function(res){ 77 | // console.log(res); 78 | t.equal(res.status, 401, "should NOT validate using BAD token"); 79 | t.end(); 80 | }); 81 | }); 82 | 83 | test("validation fail (invalid token)", function (t) { 84 | mock.req.headers.authorization = 'malformed token'; // we got this above 85 | lib.validate(mock.req, mock.res, function(res){ 86 | t.equal(res.status, 401, "should NOT validate using INVALID token"); 87 | t.end(); 88 | }); 89 | }); 90 | 91 | 92 | test("validate", function (t) { 93 | mock.req.headers.authorization = token; // we got this above 94 | // console.log(lib.verify(token)); 95 | // console.log(token); 96 | lib.validate(mock.req, mock.res, function(res){ 97 | // console.log(res); 98 | t.equal(res.status, 200, "should validate using token"); 99 | t.end(); 100 | }); 101 | }); 102 | 103 | test("logout", function (t) { 104 | lib.logout(mock.req, mock.res, function(res){ 105 | // console.log(res.status); 106 | t.equal(res.status, 200, "Logged out"); 107 | t.end(); 108 | }); 109 | }); 110 | 111 | test("no access after logout", function (t){ 112 | // confirm cannot access restricted content anymore: 113 | mock.req.headers.authorization = token; // we got this above 114 | lib.validate(mock.req, mock.res, function(res){ 115 | // console.log(res); 116 | t.equal(res.status, 401, "No longer has access to private content!"); 117 | t.end(); 118 | }); 119 | }); 120 | 121 | 122 | test("malicious logout", function (t) { 123 | mock.req.headers.authorization = 'malformed token'; // we got this above 124 | lib.logout(mock.req, mock.res, function(res){ 125 | // console.log(res.status); 126 | t.equal(res.status, 401, "Logged out"); 127 | // mock.res.end('end'); 128 | t.end(); 129 | }); 130 | }); 131 | 132 | test("notFound", function (t) { 133 | var res = lib.notFound(mock.res); 134 | // console.log(res.status); 135 | t.equal(res.status, 404, "Not found"); 136 | t.end(); 137 | }); 138 | 139 | // Most of the previous test cases are part of a 140 | // a well-defined flow and thus depend on each other (and the tokens 141 | // available in the db). Therefore, since this test needs to add 142 | // a customized token to db, keeping this as the last one to avoid 143 | // accidentally interrupting that main flow. 144 | // 145 | // Ensure that the test case doesn't timeout too soon by tape 146 | test("validation fail (expired token)", { timeout: 1500 }, function (t) { 147 | var token = lib.generateAndStoreToken(mock.req, { 148 | expires: '1s' 149 | }); 150 | 151 | mock.req.headers.authorization = token; 152 | 153 | setTimeout(function() { 154 | lib.validate(mock.req, mock.res, function(res){ 155 | t.equal(res.status, 401, "should NOT validate using EXPIRED token"); 156 | t.end(); 157 | }); 158 | }, 1100); // check validity right after the token has expired 159 | }); 160 | 161 | setTimeout(function(){ 162 | lib.done(mock.res); 163 | lib.exit(mock.res); 164 | }, 1500); 165 | 166 | process.on('uncaughtException', function(err) { 167 | console.log('FAIL ... ' + err); 168 | }); 169 | -------------------------------------------------------------------------------- /example/test/integration.js: -------------------------------------------------------------------------------- 1 | var test = require('tape'); 2 | var request = require('request'); 3 | var qs = require('querystring'); 4 | var host = "http://127.0.0.1:"; 5 | var port = Math.floor(Math.random() * 9000) + 1000; 6 | 7 | process.env.PORT = port; 8 | // var server = require('../server'); 9 | 10 | var exec = require('child_process').exec; 11 | exec('node ./example/server.js', function(error, stdout, stderr) { 12 | // console.log(stdout); // uncomment for console.log 13 | // console.log('stderr: ', stderr); // uncomment for errors 14 | if (error !== null) { 15 | console.log('exec error: ', error); 16 | } 17 | 18 | }); 19 | 20 | setTimeout(function() { // only run tests once child_process has started 21 | 22 | test("Connect to localhost "+host+":"+port, function (t) { 23 | request(host+port ,function (err, res, body) { 24 | // console.log(err); 25 | // t.equal(err, null, "No Errors Connecting"); 26 | // console.log(res); 27 | t.equal(res.statusCode, 200, "Status 200"); 28 | t.end(); 29 | }); 30 | }); 31 | 32 | // attempt to access content before being authenticated 33 | test("Attempt auth "+host+port+"/auth (incorrect username/password should fail)", function (t) { 34 | 35 | var form = { 36 | username: 'lordbusiness', 37 | password: 'kragle', 38 | }; 39 | 40 | var formData = qs.stringify(form); 41 | var contentLength = formData.length; 42 | 43 | var options = { 44 | headers: { 45 | 'Content-Length': contentLength, 46 | 'Content-Type': 'application/x-www-form-urlencoded', 47 | 'user-agent': 'Mozilla/5.0' 48 | }, 49 | uri: host+port+"/auth", 50 | body: formData, 51 | method: 'POST' 52 | } 53 | 54 | request.post(options ,function (err, res, body) { 55 | t.equal(res.statusCode, 401, "Cannot authenticate (incorrect un/pw)"); 56 | t.end(); 57 | }); 58 | }); 59 | 60 | test("Attempt to access restricted content: "+host+port+"/private without supplying a valid token!", function (t) { 61 | 62 | var options = { 63 | headers: { 64 | 'authorization': 'invalid', 65 | 'user-agent': 'Mozilla/5.0' 66 | }, 67 | uri: host+port+"/private", 68 | method: 'GET' 69 | } 70 | 71 | request(options ,function (err, res, body) { 72 | t.equal(res.statusCode, 401, "Private content access denied!"); 73 | t.end(); 74 | }); 75 | }); 76 | 77 | 78 | var token = null; // starts out empty && Yes, GLOBAL (its a test!) 79 | 80 | test("Authenticate "+host+port+"/auth", function (t) { 81 | 82 | var form = { 83 | username: 'masterbuilder', 84 | password: 'itsnosecret', 85 | }; 86 | 87 | var formData = qs.stringify(form); 88 | var contentLength = formData.length; 89 | 90 | var options = { 91 | headers: { 92 | 'Content-Length': contentLength, 93 | 'Content-Type': 'application/x-www-form-urlencoded', 94 | 'user-agent': 'Mozilla/5.0' 95 | }, 96 | uri: host+port+"/auth", 97 | body: formData, 98 | method: 'POST' 99 | } 100 | 101 | request.post(options ,function (err, res, body) { 102 | token = res.headers.authorization; // save the token for later 103 | t.equal(res.statusCode, 200, "Authenticated"); 104 | t.end(); 105 | }); 106 | }); 107 | 108 | test("Access restricted content: "+host+port+"/private", function (t) { 109 | 110 | var options = { 111 | headers: { 112 | 'authorization': token, 113 | 'user-agent': 'Mozilla/5.0' 114 | }, 115 | uri: host+port+"/private", 116 | method: 'GET' 117 | } 118 | 119 | request(options ,function (err, res, body) { 120 | t.equal(res.statusCode, 200, "Private content accessed"); 121 | t.end(); 122 | }); 123 | }); 124 | 125 | // simulate logging out (reset token) >> how do we Invalidate it...? 126 | test("Log out "+host+port+"/logout", function (t) { 127 | var options = { 128 | headers: { 129 | 'authorization': token, 130 | 'user-agent': 'Mozilla/5.0' 131 | }, 132 | uri: host+port+"/logout", 133 | method: 'GET' 134 | } 135 | request(options ,function (err, res, body) { 136 | t.equal(body, 'Logged Out!', "Exit server"); 137 | t.equal(res.statusCode, 200, "End tests!"); 138 | t.end(); 139 | }); 140 | }); 141 | 142 | // simulate logging out (reset token) >> how do we Invalidate it...? 143 | test("Attempt access using expired token (after logout)", function (t) { 144 | var options = { 145 | headers: { 146 | 'authorization': token, 147 | 'user-agent': 'Mozilla/5.0' 148 | }, 149 | uri: host+port+"/private", 150 | method: 'GET' 151 | } 152 | request(options ,function (err, res, body) { 153 | // t.equal(body, 'Logged Out!', "Exit server"); 154 | t.equal(res.statusCode, 401, "Access Denied! (as expected)"); 155 | t.end(); 156 | }); 157 | }); 158 | 159 | 160 | test("EXIT "+host+port+"/exit", function (t) { 161 | request(host+port+"/exit" ,function (err, res, body) { 162 | t.equal(body, 'bye', "Exit server"); 163 | t.equal(res.statusCode, 404, "End tests!"); 164 | t.end(); 165 | }); 166 | }); 167 | 168 | }, 442); // give the server time to start 169 | 170 | 171 | process.on('uncaughtException', function(err) { 172 | console.log('FAIL ... ' + err); 173 | }); 174 | -------------------------------------------------------------------------------- /example/test/mock.js: -------------------------------------------------------------------------------- 1 | // both the request and response http objects are event emitters 2 | var events = require('events'); // lets use the core node.js event emmitter 3 | var req = new events.EventEmitter(); // gives us req.emit and req.on('data') 4 | var res = new events.EventEmitter(); // gives us res.emit and req.on('data') 5 | 6 | // mock methods for http request & response 7 | // request should have: 8 | // req.headers 9 | // just set the desired headers object before invoking the function 10 | // here are a few defaults 11 | req.headers = { 12 | 'Content-Type': 'text/html', 13 | 'user-agent': 'Mozilla/5.0', 14 | } 15 | 16 | // req.method e.g: POST or GET 17 | // default to POST cause our check is for this method in authHandler 18 | req.method = 'POST'; 19 | 20 | // req.on('data' ... borrowed from event emitter (see above) 21 | // req.on('end' ... event emitter (again) 22 | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 23 | 24 | // response shoud have 25 | // res.writeHead( 26 | res.writeHead = function(status, headers) { 27 | res = res || {}; 28 | res.headers = headers; 29 | res.status = status; 30 | return res; 31 | } 32 | 33 | // res.end( 34 | res.end = function(str) { 35 | res = res || {}; 36 | res.body = str; 37 | return res; 38 | } 39 | 40 | 41 | module.exports = { 42 | req : req, 43 | res : res 44 | } 45 | -------------------------------------------------------------------------------- /example/views/fail.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 | 6 |