├── .gitignore ├── .jshintrc ├── LICENSE ├── README.md ├── UPGRADING.md ├── index.js ├── lib ├── MiddlewareContext.js └── middleware │ ├── authenticate.js │ ├── authenticateApiKeyForToken.js │ ├── authenticateBasicAuth.js │ ├── authenticateBearerAuthorizationHeader.js │ ├── authenticateCookie.js │ ├── authenticateForToken.js │ ├── authenticateSocialForToken.js │ ├── authenticateUsernamePasswordForToken.js │ ├── corsHandler.js │ ├── currentUser.js │ ├── groupRequired.js │ ├── groupsRequired.js │ ├── logout.js │ ├── passwordReset.js │ ├── register.js │ ├── resendEmailVerification.js │ ├── verifyEmailVerificationToken.js │ └── writeToken.js ├── package.json ├── properties.json └── test ├── authenticate.js ├── authenticateBearerAuthorizationHeader.js ├── authenticateForToken.js ├── authenticateSocialForToken.js ├── authenticateUsernamePasswordForToken.js ├── cookie-options.js ├── cors.js ├── custom-error-handlers.js ├── email-verification-endpoints.js ├── fixtures └── loginSuccess.js ├── helpers.js ├── index.js ├── it-fixtures ├── README.md └── loader.js ├── password-reset-endpoint.js ├── token-endpoint.js └── write-tokens-option.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /coverage 3 | npm-debug.log 4 | *.ipr 5 | *.iws 6 | *.iml 7 | .idea 8 | .DS_Store 9 | test/it-fixtures/*.json 10 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "curly": true, 3 | "eqeqeq": true, 4 | "immed": true, 5 | "latedef": true, 6 | "newcap": true, 7 | "noarg": true, 8 | "sub": true, 9 | "undef": true, 10 | "unused": true, 11 | "boss": true, 12 | "eqnull": true, 13 | "node": true, 14 | "globals": { 15 | /* MOCHA */ 16 | "describe": false, 17 | "it": false, 18 | "before": false, 19 | "beforeEach": false, 20 | "after": false, 21 | "afterEach": false 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #Stormpath is Joining Okta 2 | We are incredibly excited to announce that [Stormpath is joining forces with Okta](https://stormpath.com/blog/stormpaths-new-path?utm_source=github&utm_medium=readme&utm-campaign=okta-announcement). Please visit [the Migration FAQs](https://stormpath.com/oktaplusstormpath?utm_source=github&utm_medium=readme&utm-campaign=okta-announcement) for a detailed look at what this means for Stormpath users. 3 | 4 | We're available to answer all questions at [support@stormpath.com](mailto:support@stormpath.com). 5 | 6 | # Stormpath Express.js SDK [DEPRECATED] 7 | 8 | [![NPM Version](https://img.shields.io/npm/v/stormpath-sdk-express.svg?style=flat)](https://npmjs.org/package/stormpath-sdk-express) 9 | [![NPM Downloads](http://img.shields.io/npm/dm/stormpath-sdk-express.svg?style=flat)](https://npmjs.org/package/stormpath-sdk-express) 10 | [![Build Status](https://img.shields.io/travis/stormpath/stormpath-sdk-express.svg?style=flat)](https://travis-ci.org/stormpath/stormpath-sdk-express) 11 | 12 | ## DEPRECATION NOTICE 13 | 14 | This module has been replaced by [express-stormpath][] and will no longer be 15 | maintained. Please see [UPGRADING.md][] for detailed information on how to 16 | migrate your application to the [express-stormpath][] module. 17 | 18 | If you need assistance, please contact support@stormpath.com - we're happy to help! 19 | 20 | If you were using this module with [stormpath-sdk-angular][], you will 21 | you will have to use version 0.6.0 of [stormpath-sdk-angular][] if you wish 22 | to continue using this module. If you want to upgrade [stormpath-sdk-angular][] 23 | to 0.7.0, you will need to start using [express-stormpath][]. 24 | 25 | ## Documentation [DEPRECATED] 26 | 27 | The primary use-case of this module is to 28 | provide a lean set of Express.js middleware that supports our new 29 | [Stormpath AngularJS SDK](https://github.com/stormpath/stormpath-angular) 30 | 31 | This module does not provide any built-in views or Angular code, it is meant 32 | to be a back-end authentication and authorization API for a Single-Page Application. If you are 33 | starting a project from scratch and would like some guidance with creating 34 | an AngularJS application, please see our 35 | [Stormpath AngularJS Guide](http://docs.stormpath.com/angularjs/guide/index.html) 36 | 37 | If you are looking for a comprehensive Express.js solution which includes a 38 | server-side page templating system, please visit our [Stormpath-Express] 39 | integration. Note: we do not yet have an integration strategy for using 40 | AngularJS with [Stormpath-Express]. 41 | 42 | **The following features are supported by this module:** 43 | 44 | * Register new accounts 45 | * Login users via username & password, Oauth Bearer Token, or Basic Auth 46 | * Email verification workflow 47 | * Password Reset Workflow 48 | 49 | **These features are NOT yet supported (but coming soon!):** 50 | 51 | * Social login 52 | * ID Site Integration 53 | 54 | If you have questions or feedback about this library, please get in touch and share your 55 | thoughts! support@stormpath.com 56 | 57 | 58 | [Stormpath](https://stormpath.com) is a User Management API that reduces 59 | development time with instant-on, scalable user infrastructure. Stormpath's 60 | intuitive API and expert support make it easy for developers to authenticate, 61 | manage, and secure users and roles in any application. 62 | 63 | ## Table of Contents 64 | 65 | * [Usage](#usage) 66 | * [Installation](#usage-installation) 67 | * [Use with all routes](#usage-all) 68 | * [Use with some routes](#usage-some) 69 | * [Complex usage](#usage-complex) 70 | 71 | * [`spConfig` Options](#spConfig) 72 | * [accessTokenCookieName](#access-token-cookie) 73 | * [accessTokenTtl](#accessTokenTtl) 74 | * [allowedOrigins](#allowedOrigins) 75 | * [endOnError](#error-handlers) 76 | * [forceHttps](#forceHttps) 77 | * [postRegistrationHandler](#postRegistrationHandler) 78 | * [scopeFactory](#scopeFactory) 79 | * [spClient](#spClient) 80 | * [tokenEndpoint](#tokenEndpoint) 81 | * [writeAccessTokenToCookie](#access-token-cookie) 82 | * [writeAccessTokenResponse](#access-token-response) 83 | * [xsrf](#XSRF) 84 | 85 | * [Custom Token Strategies](#custom-token-strategies) 86 | 87 | * [Middleware API](#middleware-api) 88 | * [authenticate](#authenticate) 89 | * [authenticateApiKeyForToken](#authenticateApiKeyForToken) 90 | * [authenticateBasicAuth](#authenticateBasicAuth) 91 | * [authenticateBearerAuthorizationHeader](#authenticateBearerAuthorizationHeader) 92 | * [authenticateCookie](#authenticateCookie) 93 | * [authenticateForToken](#authenticateForToken) 94 | * [authenticateUsernamePasswordForToken](#authenticateUsernamePasswordForToken) 95 | * [authenticateSocialForToken](#authenticateSocialForToken) 96 | * [groupsRequired](#groupsRequired) 97 | * [logout](#logout) 98 | * [writeToken](#writeToken) 99 | 100 | * [Middleware Factory](#middleware-factory) 101 | 102 | * [Types](#types) 103 | * [Account](#Account) 104 | * [AuthenticationRequest](#AuthenticationRequest) 105 | * [Jwt](#Jwt) 106 | * [TokenResponse](#TokenResponse) 107 | 108 | 109 | 110 | 111 | ## Usage 112 | 113 | #### Install via NPM 114 | 115 | ```bash 116 | npm install --save stormpath-sdk-express 117 | ``` 118 | 119 | The features in this library depend on cookies and POST body 120 | data. For that reason, you should use [cookie-parser] or [body-parser] or 121 | any other library that sets `req.cookies` and `req.body`. If you need 122 | to install them: 123 | 124 | ```bash 125 | npm install --save cookie-parser body-parser 126 | ``` 127 | 128 | 129 | 130 | 131 | #### Use with all routes 132 | 133 | 134 | To use the library with default options, you will do the follwing: 135 | 136 | * Require the module 137 | * Create an instance of `spMiddlware` 138 | * Attach the default routes 139 | * Use the [`authenticate`](#authenticate) middleware on all your 140 | routes, via `app.use()` 141 | 142 | **Basic Usage Example:** 143 | ```javascript 144 | var cookieParser = require('cookie-parser'); 145 | var bodyParser = require('body-parser'); 146 | var express = require('express'); 147 | var stormpathExpressSdk = require('stormpath-sdk-express'); 148 | 149 | var spConfig = { 150 | appHref: 'YOUR_STORMPATH_APP_HREF', 151 | apiKeyId: 'YOUR_STORMPATH_API_KEY_ID', 152 | apiKeySecret: 'YOUR_STORMPATH_API_KEY_SECRET' 153 | } 154 | 155 | var app = express(); 156 | 157 | app.use(cookieParser()); 158 | app.use(bodyParser.json()); 159 | 160 | var spMiddleware = stormpathExpressSdk.createMiddleware(spConfig); 161 | 162 | spMiddleware.attachDefaults(app); 163 | 164 | app.use(spMiddleware.authenticate); 165 | ``` 166 | 167 | Doing this will enable the following functionality: 168 | 169 | * All endpoints in your Express application will first be routed through the 170 | [`authenticate`](#authenticate) middleware, which will assert that the user has 171 | an existing access token. If this is not true, an error response will be sent. 172 | 173 | * The endpoint `/oauth/token` will accept Client Password, Client Credential, or 174 | Social Login grant requests and respond with an access token if authentication is successful. 175 | Depending on the grant type (`password`, `client_credentials`, or 'social') it delegates to 176 | [`authenticateUsernamePasswordForToken`](#authenticateUsernamePasswordForToken) 177 | , [`authenticateApiKeyForToken`](#authenticateApiKeyForToken) or 178 | [`authenticateSocialForToken`](#authenticateSocialForToken). 179 | 180 | * The `/logout` endpoint will be provided for ending cookie-based sessions. 181 | 182 | 183 | * The [XSRF Protection](#XSRF) feature will be enabled. After authentication, 184 | all POST requests will validated with an XSRF token as well. 185 | 186 | 187 | #### Use with some routes 188 | 189 | If you don't need to authenticate all routes, but still want to use token 190 | authentication, then don't use the statement `app.use(spMiddleware.authenticate)`. 191 | Instead, use the [`authenticate`](#authenticate) middleware on the 192 | routes that need authentication: 193 | 194 | **Specific Route Example:** 195 | ```javascript 196 | // Enforce authentication on the API 197 | 198 | app.get('/api/*',spMiddleware.authenticate,function(req,res,next){ 199 | // If we get here, the user has been authenticated 200 | // The account object is available at req.user 201 | }); 202 | ``` 203 | 204 | #### More complex usage 205 | 206 | If you have a more complex use case, we suggest: 207 | 208 | * Using the [`spConfig`](#spConfig) options to control features 209 | * Custom chaining of specific middleware from the [Middleware API](#middleware-api) 210 | * Create custom middleware using the [Middleware Factory Methods](#middleware-factory) 211 | * Creating multiple instances of `spMiddleware` with different [`spConfig`](#spConfig) options 212 | * Using the Router object in Express 4.x to organize your routes 213 | 214 | 215 | ## `spConfig` options 216 | 217 | The default behavior of the middleware can be modified via the `spCpnfig` object 218 | that you pass to `createMiddleware()`. This section describes all the 219 | available options and which middleware functions they affect. 220 | 221 | 222 | 223 | 224 | #### Access Token TTL (seconds) 225 | 226 | The default duration of access tokens is one hour. Use `accessTokenTtl` to 227 | set a different value in seconds. Once the token expires, the client 228 | will need to obtain a new token. 229 | 230 | ```javascript 231 | var spConfig = { 232 | accessTokenTtl: 60 * 60 * 24 // One day (24 hours) 233 | } 234 | ``` 235 | 236 | Used by [`authenticateUsernamePasswordForToken`](#authenticateUsernamePasswordForToken), [`authenticateApiKeyForToken`](#authenticateApiKeyForToken), [`authenticateSocialForToken`](#authenticateSocialForToken) 237 | 238 | 239 | 240 | 241 | #### Access Token Cookie 242 | 243 | The default response to a successful password grant request is to create an 244 | access token and write it to a secure cookie. The name of the cookie will be 245 | `access_token` and the value will be a JWT token. 246 | 247 | Set `{ writeAccessTokenToCookie: false }` if you do not want to write the access 248 | token to a cookie. 249 | 250 | Use `accessTokenCookieName` to change the default name of the cookie. 251 | 252 | **Example: disable access token cookies** 253 | ```javascript 254 | var spConfig = { 255 | writeAccessTokenToCookie: false 256 | } 257 | ``` 258 | 259 | **Example: custom access token cookie name** 260 | ```javascript 261 | var spConfig = { 262 | accessTokenCookieName: 'custom_cookie_name' 263 | } 264 | ``` 265 | 266 | Used by [`authenticateUsernamePasswordForToken`](#authenticateUsernamePasswordForToken), [`authenticateCookie`](#authenticateCookie), [`authenticateSocialForToken`](#authenticateSocialForToken) 267 | 268 | 269 | 270 | 271 | #### Access Token Response 272 | 273 | By default, this library will not send the access token in the 274 | body of the response if the grant type is `password`. Instead it will 275 | write the access token to an HTTPS-only cookie and send 201 for the response status. 276 | 277 | This is for security purposes. By exposing it in the body, you expose it 278 | to the javascript environment on the client, which is vulnerable to XSS 279 | attacks. 280 | 281 | You can disable this security feature by enabling the access token response writer: 282 | 283 | ```javascript 284 | var spConfig = { 285 | writeAccessTokenResponse: true 286 | } 287 | ``` 288 | 289 | When enabled, the response body will be a [`TokenResponse`](#TokenResponse) 290 | 291 | Used by [`authenticateUsernamePasswordForToken`](#authenticateUsernamePasswordForToken), [`authenticateSocialForToken`](#authenticateSocialForToken) 292 | 293 | 294 | 295 | 296 | #### Allowed Origins 297 | 298 | This is a whitelist of domains that are allowed to make requests of your API. 299 | If you are making cross-origin requests (CORS) to your server, you will need to 300 | whitelist the origin domains with this option. This library will automatically 301 | respond with the relevant response headers that are required by CORS. 302 | 303 | ```javascript 304 | var spConfig = { 305 | allowedOrigins: ['http://example.com'] 306 | } 307 | ``` 308 | 309 | Used by `autoRouteHandler` via `app.use(stormpathMiddleware)` 310 | 311 | #### Error Handlers 312 | 313 | By default, the library will respond to failed authentication by ending the 314 | response and sending an appropriate error code and message. 315 | 316 | Set `endOnError` to false to disable this default behaviour. 317 | 318 | In this case, the library will assign the error to `req.authenticationError` 319 | and continue the middleware chain. 320 | 321 | ```javascript 322 | var spConfig = { 323 | endOnError: false 324 | } 325 | ``` 326 | 327 | Used by `handleAuthenticationError` 328 | 329 | **Example: custom error handling** 330 | 331 | ```javascript 332 | app.get('/secrets',spMiddleware.authenticate,function(req,res,next){ 333 | if(req.authenticationError){ 334 | res.json(401,{ 335 | error: req.authenticationError 336 | }); 337 | }else{ 338 | res.json(200,{ 339 | message: 'Hello, ' + req.user.fullName 340 | }); 341 | } 342 | }); 343 | ``` 344 | 345 | 346 | 347 | #### HTTPS 348 | 349 | This library auto-negotiates HTTP vs HTTPS. If your server is accepting 350 | HTTPS requests, it will automatically add the `Secure` option to the 351 | access token cookie. This relies on [`req.protocol`][express.req.protocol] from the Express 352 | framework. 353 | 354 | If your server is behind a load balancer, you should use the [`"trust proxy"`][trust-proxy-option] 355 | option for Express. 356 | 357 | If you cannot trust that your forward proxy is doing the right thing, then you 358 | should force this library to always use HTTPS. 359 | 360 | **Example: force HTTPS always** 361 | 362 | ```javascript 363 | var spConfig = { 364 | forceHttps: true 365 | } 366 | ``` 367 | Used by [`writeToken`](#writeToken) 368 | 369 | 370 | 371 | #### Post Registration Handler 372 | 373 | Use this handler to receive account objects that have been created. This handler 374 | is called after a POST to `/register` has resulted in a new account. You must 375 | end the response manually, or call `next()` to allow the default 376 | responder to finish the response. 377 | 378 | The newly created user is available at `req.user`. 379 | 380 | **Example: Add some custom data after registration** 381 | 382 | ```javascript 383 | var spConfig = { 384 | postRegistrationHandler: function(req,res,next){ 385 | req.user.getCustomData(function(err,customData){ 386 | if(err){ 387 | res.end(err) 388 | }else{ 389 | customData.myCustomValue = 'foo'; 390 | customData.save(function(err){ 391 | if(err){ 392 | res.end(err) 393 | }else{ 394 | next(); 395 | } 396 | }) 397 | } 398 | }); 399 | } 400 | } 401 | ``` 402 | 403 | **Example: Send a custom registration response** 404 | 405 | ```javascript 406 | var spConfig = { 407 | postRegistrationHandler: function(req,res,next){ 408 | res.end("Thank you for registering"); 409 | } 410 | } 411 | ``` 412 | 413 | #### Scope Factory 414 | 415 | By default, the library will not add scope to access tokens, we leave this 416 | in your control. Implement a scope factory if you wish to respond to a requested scope by granting 417 | specific scopes. It will be called before the access token 418 | is written. You MUST call the done callback with the granted scope. If 419 | your logic determines that no scope should be given, simply call back 420 | with a falsey value. 421 | 422 | If you need more control over the token creation, see 423 | [Custom Token Strategies](#custom-token-strategies) 424 | 425 | ```javascript 426 | 427 | var spConfig = { 428 | // ... 429 | scopeFactory: function(req,res,authenticationResult,account,requestedScope,done){ 430 | 431 | // requestedScope is a string passed in from the request 432 | 433 | var grantedScope = ''; 434 | 435 | // do your logic, then callback with an error or the granted scope 436 | 437 | done(null,grantedScope) 438 | 439 | } 440 | } 441 | ``` 442 | 443 | 444 | 445 | #### spClient 446 | 447 | This is a reference to the Stormpath Client that was created by this module, 448 | based on the [`spConfig`](#spConfig) that was passed. 449 | 450 | This is the client that is exported by the 451 | [Stormpath Node SDK][stormpath-node-sdk], you can use it to achieve all the 452 | Stormpath API functionality that is supported by that SDK. Full documentation 453 | here: 454 | 455 | http://docs.stormpath.com/nodejs/api/home 456 | 457 | 458 | #### Token Endpoint 459 | 460 | Defines the endpoint that is auto-bound to [`authenticateForToken`](#authenticateForToken). 461 | The binding only happens if you call `app.use(spMiddleware)`. 462 | 463 | ```javascript 464 | var spConfig = { 465 | tokenEndpoint: '/my/alt/token/endpoint' 466 | } 467 | ``` 468 | 469 | 470 | 471 | 472 | #### XSRF Protection 473 | 474 | This library combats [Cross-Site Request Forgery] by issuing XSRF tokens. 475 | They are issued at the time an access token is created. The value of the XSRF 476 | token is written to the `XSRF-TOKEN` cookie and stored as the `xsrfToken` claim 477 | within the access token. Doing this allows the library to do a stateless 478 | check when validating XSRF tokens. 479 | 480 | Your client application, for any POST request, should supply the XSRF token 481 | via the `X-XSRF-TOKEN: ` HTTP Header. If you are using Angular.js, this 482 | will happen for you automatically. 483 | 484 | If you do not want to issue or validate XSRF tokens, disable the feature: 485 | 486 | ```javascript 487 | var spConfig = { 488 | xsrf: false 489 | } 490 | ``` 491 | 492 | 493 | 494 | 495 | ## Custom Token Strategies 496 | 497 | This library will issue JWT access tokens with a configurable TTL and scope. 498 | All other values (`sub`, `iat`, etc.) are set automatically and 499 | used for verifying the integrity of the token during authentication. 500 | 501 | If you want to implement your own token responder (not recommended!), you can 502 | do so by setting the `{ writeTokens: false }` option in [`spConfig`](#spConfig) 503 | 504 | Doing this will prevent the library from automatically generating tokens and sending 505 | them in responses. Instead, it will provide an [`AuthenticationRequest`](#AuthenticationRequest) 506 | object at `req.authenticationRequest` and call `next()`. It is 507 | up to you to generate a token and end the response. 508 | 509 | **NOTE:** Disabling token creation will also disable the creation and 510 | validation of XSRF tokens. 511 | 512 | 513 | ## Middleware API 514 | 515 | This section is a list of of middleware functions that are available 516 | on the object that is returned by `createMiddleware(spConfig)` 517 | 518 | When you call `createMiddleware(spConfig)`, you are simply creating a set 519 | of middleware functions which are pre-bound to the Stormpath Application that 520 | you define with the `spConfig` options. You can then mix-and-match these 521 | middleware functions to create authentication logic that suits your 522 | application. 523 | 524 | **WARNING**: We have put a lot of thought into the default decisions of this 525 | library. While we provide this API for developer use, it is required that 526 | you understand the security trade-offs that you may be making by customizing 527 | this library. If you need any assistance, please contact support@stormpath.com. 528 | 529 | ### authenticate 530 | 531 | This is a convenience middleware that will inspect the request and call 532 | [`authenticateCookie`](#authenticateCookie) or 533 | [`authenticateBearerAuthorizationHeader`](#authenticateBearerAuthorizationHeader) or 534 | [`authenticateBasicAuth`](#authenticateBasicAuth) 535 | for you 536 | 537 | **Example usage: authenticate specific routes** 538 | 539 | ````javascript 540 | var app = express(); 541 | 542 | var spMiddleware = stormpathExpressSdk.createMiddleware({/* options */}) 543 | 544 | app.get('/something',spMiddleware.authenticate,function(req,res,next){ 545 | res.json({message: 'Hello, ' + req.user.fullName}); 546 | }); 547 | ```` 548 | 549 | 550 | 551 | 552 | ### authenticateCookie 553 | 554 | Looks for an access token in the request cookies. 555 | 556 | If authenticated, assigns an [`Account`](#Account) to `req.user` and provides 557 | the unpacked access token at `req.accessToken` (an instance of [`Jwt`](#Jwt)) 558 | 559 | If an error is encountered, it ends the response with an error. If 560 | using the option `{ endOnError: false}`, it sets 561 | `req.authenticationError` and continues the chain. 562 | 563 | **Example: use cookie authentication for a specific endpoint** 564 | ````javascript 565 | var spMiddleware = stormpathExpressSdk.createMiddleware({/* options */}) 566 | 567 | var app = express(); 568 | 569 | app.get('/something',spMiddleware.authenticateCookie,function(req,res,next){ 570 | res.json({ 571 | message: 'Hello, ' + req.user.fullName + ', ' + 572 | 'you have a valid access token in a cookie. ' + 573 | 'Your token expires in: ' + req.accessToken.body.exp 574 | }); 575 | }); 576 | ```` 577 | 578 | 579 | 580 | ### authenticateBasicAuth 581 | 582 | Looks for an access token in the `Authorization: Basic ` header 583 | on the request 584 | 585 | If authenticated, assigns an [`Account`](#Account) to `req.user` and provides 586 | the unpacked access token at `req.accessToken` (an instance of [`Jwt`](#Jwt)) 587 | 588 | If an error is encountered, it ends the response with an error. If 589 | using the option `{ endOnError: false}`, it sets 590 | `req.authenticationError` and continues the chain. 591 | 592 | **Example: use Basic Authentication for a specific endpoint** 593 | ````javascript 594 | var spMiddleware = stormpathExpressSdk.createMiddleware({/* options */}) 595 | var app = express(); 596 | 597 | app.use(spMiddleware) 598 | 599 | app.get('/something',spMiddleware.authenticateBasicAuth,function(req,res,next){ 600 | res.json({ 601 | message: 'Hello, ' + req.user.fullName + ', ' + 602 | 'you have a valid API Key in your Basic Authorization header. ' + 603 | 'Your token expires in: ' + req.accessToken.body.exp 604 | }); 605 | }); 606 | ```` 607 | 608 | 609 | 610 | 611 | ### authenticateBearerAuthorizationHeader 612 | 613 | Looks for an access token in the `Authorization: Bearer ` header 614 | on the request 615 | 616 | If authenticated, assigns an [`Account`](#Account) to `req.user` and provides 617 | the unpacked access token at `req.accessToken` (an instance of [`Jwt`](#Jwt)) 618 | 619 | If an error is encountered, it ends the response with an error. If 620 | using the option `{ endOnError: false}`, it sets 621 | `req.authenticationError` and continues the chain. 622 | 623 | **Example: use bearer authentication for a specific endpoint** 624 | ````javascript 625 | var spMiddleware = stormpathExpressSdk.createMiddleware({/* options */}) 626 | var app = express(); 627 | 628 | app.use(spMiddleware) 629 | 630 | app.get('/something',spMiddleware.authenticateBearerAuthorizationHeader,function(req,res,next){ 631 | res.json({ 632 | message: 'Hello, ' + req.user.fullName + ', ' + 633 | 'you have a valid access token in your Authorization header. ' + 634 | 'Your token expires in: ' + req.accessToken.body.exp 635 | }); 636 | }); 637 | ```` 638 | 639 | 640 | 641 | 642 | ### authenticateApiKeyForToken 643 | 644 | This is used with the [Stormpath API Key Authentication Feature][Api Key Authentication]. It expects an 645 | account's API Key and Secret to be supplied in HTTP headers via the HTTP 646 | Basic scheme. 647 | 648 | 649 | **Example: posting api key ID and secret to the token endpoint** 650 | ``` 651 | POST /oauth/tokens?grant_type=client_credentials 652 | Authorization: Basic 653 | ``` 654 | 655 | If the supplied credentials are a valid API Key for an account, this function will respond 656 | by writing a [`TokenResponse`](#TokenResponse) and ending the request. 657 | 658 | **Example: accept only api keys for token exchange** 659 | ````javascript 660 | var spMiddleware = stormpathExpressSdk.createMiddleware({/* options */}) 661 | 662 | var app = express(); 663 | 664 | app.post('/oauth/token',spMiddleware.authenticateApiKeyForToken); 665 | ```` 666 | 667 | 668 | 669 | 670 | ### authenticateUsernamePasswordForToken 671 | 672 | Expects a JSON POST body, which has a `username` and `password` field, and a 673 | grant type request of `password`. 674 | 675 | **Example: posting username and password to the token endpoint** 676 | ``` 677 | POST /oauth/tokens?grant_type=password 678 | 679 | { 680 | "username": "foo@bar.com", 681 | "password": "aSuperSecurePassword" 682 | } 683 | ``` 684 | 685 | If the supplied password is valid for the username, this function will respond 686 | with an access token cookie or access token response, depending on the configuration 687 | set by [`writeAccessTokenResponse`](#access-token-response) and 688 | [`writeAccessTokenToCookie`](#access-token-cookie) 689 | 690 | **Example: accept only username & password for token exchange** 691 | ````javascript 692 | var spMiddleware = stormpathExpressSdk.createMiddleware({/* options */}) 693 | 694 | var app = express(); 695 | 696 | app.post('/login',spMiddleware.authenticateUsernamePasswordForToken); 697 | ```` 698 | 699 | ### authenticateSocialForToken 700 | 701 | Expects a JSON POST body, which has a `providerId` and `accessToken` field, and a 702 | grant type request of `social`. 703 | 704 | **Example: posting providerId and accessToken to the token endpoint** 705 | ``` 706 | POST /oauth/tokens?grant_type=social 707 | 708 | { 709 | "providerId": "facebook", 710 | "accessToken": "aTokenReceivedFromFacebookLogin" 711 | } 712 | ``` 713 | 714 | If the supplied accessToken is valid for the provider, this function will respond 715 | with an access token cookie or access token response, depending on the configuration 716 | set by [`writeAccessTokenResponse`](#access-token-response) and 717 | [`writeAccessTokenToCookie`](#access-token-cookie) 718 | 719 | **Example: accept only social for token exchange** 720 | ````javascript 721 | var spMiddleware = stormpathExpressSdk.createMiddleware({/* options */}) 722 | 723 | var app = express(); 724 | 725 | app.post('/login',spMiddleware.authenticateSocialForToken); 726 | ```` 727 | 728 | 729 | 730 | #### authenticateForToken 731 | 732 | This is a convenience middleware that will inspect the request 733 | and delegate to [`authenticateApiKeyForToken`](#authenticateApiKeyForToken), 734 | [`authenticateUsernamePasswordForToken`](#authenticateUsernamePasswordForToken) 735 | or [`authenticateSocialForToken`](#authenticateSocialForToken) 736 | 737 | **Example: manually defining the token endpoint** 738 | ````javascript 739 | var spMiddleware = stormpathExpressSdk.createMiddleware({/* options */}) 740 | 741 | var app = express(); 742 | 743 | app.post('/tokens-r-us',spMiddleware.authenticateForToken); 744 | ```` 745 | Note: this example can also be accomplished with the `tokenEndpoint` option in`spConfig`. 746 | 747 | 748 | 749 | #### groupsRequired 750 | 751 | This middleware allows you to perform group-based authorization. It takes 752 | a string or array of strings. If an array, the user must exist in all said 753 | groups. 754 | 755 | **Example: manually defining the token endpoint** 756 | ````javascript 757 | var spMiddleware = stormpathExpressSdk.createMiddleware({/* options */}) 758 | 759 | var app = express(); 760 | 761 | app.get('/secrets',spMiddleware.groupsRequired('admin')); 762 | 763 | app.get('/not-so-secrets',spMiddleware.groupsRequired(['admin','editor'])); 764 | ```` 765 | Note: this example can also be accomplished with the `tokenEndpoint` option in`spConfig`. 766 | 767 | 768 | 769 | 770 | #### writeToken 771 | 772 | This method will write an access token cookie or access token body response, 773 | as configured by the [`writeAccessTokenResponse`](#access-token-response) and 774 | [`writeAccessTokenToCookie`](#access-token-cookie) options. 775 | 776 | It expects that `req.jwt` has already been populated by one of the authentication 777 | functions. 778 | 779 | **Example: manually defining the token endpoint** 780 | ```` 781 | var spMiddleware = stormpathExpressSdk.createMiddleware({/* options */}) 782 | 783 | var app = express(); 784 | 785 | app.post('/tokens-r-us',spMiddleware.writeToken); 786 | ```` 787 | Note: example can also be accomplished with the `tokenEndpoint` option in `spConfig`. 788 | 789 | 790 | 791 | 792 | #### logout 793 | 794 | This method will delete the XSRF and access token cookies on the client. 795 | 796 | **Example: manually defining a logout endpoint** 797 | ```` 798 | var spMiddleware = stormpathExpressSdk.createMiddleware({/* options */}) 799 | 800 | var app = express(); 801 | 802 | app.get('/logout',spMiddleware.logout); 803 | ```` 804 | 805 | 806 | 807 | 808 | ## Middleware Factory Methods 809 | 810 | If you only need certain features or middleware functions, you can construct 811 | the middleware functions manually. All middleware functions can be constructed 812 | by calling their constructor directly off the library export. Simply 813 | use a capital letter to access the constructor. When calling the constructor, 814 | you must provide an `spConfig` object with relevant options. 815 | 816 | **WARNING**: We have put a lot of thought into the default decisions of this 817 | library. While we provide this API for developer use, it is required that 818 | you understand the security trade-offs that you may be making but customizing 819 | this library. If you need any assistance, please contact support@stormpath.com 820 | 821 | For example: say you want one web service to issue access tokens for API 822 | Authentication. You then want all your other web services to read 823 | these tokens and use them for authentication. 824 | 825 | **Example: a token generation webservice** 826 | ```javascript 827 | var express = require('express'); 828 | var stormpathSdkExpress = require('stormpath-sdk-express'); 829 | 830 | var spConfig = { 831 | appHref: '', 832 | apiKeyId: '', 833 | apiKeySecret: '', 834 | } 835 | 836 | var tokenExchanger = stormpathSdkExpress.AuthenticateApiKeyForToken(spConfig); 837 | 838 | var app = express(); 839 | 840 | app.post('/oauth/tokens',tokenExchanger); 841 | ``` 842 | 843 | **Example: authenticate tokens in your other web services** 844 | ```javascript 845 | var express = require('express'); 846 | var stormpathSdkExpress = require('stormpath-sdk-express'); 847 | 848 | var spConfig = { 849 | appHref: '', 850 | apiKeyId: '', 851 | apiKeySecret: '', 852 | } 853 | 854 | var authenticate = stormpathSdkExpress.Authenticate(spConfig); 855 | 856 | var app = express(); 857 | 858 | app.use('/api/*',authenticate,function(req,res,next){ 859 | // handle api request. Account will be available at req.user 860 | }); 861 | ``` 862 | 863 | 864 | 865 | ## Types 866 | 867 | These are the object types that you will find in this library. 868 | 869 | ### Account 870 | 871 | This object is provided by the underlying [Stormpath Node SDK][stormpath-node-sdk], 872 | it is documented here: 873 | 874 | http://docs.stormpath.com/nodejs/api/account 875 | 876 | ### AuthenticationRequest 877 | 878 | This object is provided by the underlying [Stormpath Node SDK][stormpath-node-sdk]. 879 | It is documented here: 880 | 881 | http://docs.stormpath.com/nodejs/api/authenticationResult 882 | 883 | ### Jwt 884 | 885 | These are objects that represent a JWT token. They have methods for manipulating 886 | the token and compacting it to an encoded string. These instances are provided by 887 | the [nJwt Library][nJwt]. 888 | 889 | ### TokenResponse 890 | 891 | This object encapsulates the compacted JWT, exposes the scope of the token, 892 | and declares the expiration time as seconds from now. 893 | 894 | **Example: token response format** 895 | ```json 896 | { 897 | "access_token": "eyJ0eXAiOiJKV1QiLCJhbGc ...", 898 | "expires_in": 3600, 899 | "token_type": "Bearer", 900 | "scope": "given-scope" 901 | } 902 | ``` 903 | 904 | ## Contributing 905 | 906 | In lieu of a formal style guide, take care to maintain the existing coding 907 | style. Add unit tests for any new or changed functionality. Lint and test 908 | your code using [Grunt](http://gruntjs.com/). 909 | 910 | You can make your own contributions by forking the develop branch of this 911 | repository, making your changes, and issuing pull request on the develop branch. 912 | 913 | We regularly maintain this repository, and are quick to review pull requests and 914 | accept changes! 915 | 916 | We <333 contributions! 917 | 918 | 919 | ## Copyright 920 | 921 | Copyright © 2015 Stormpath, Inc. and contributors. 922 | 923 | This project is open-source via the [Apache 2.0 License](http://www.apache.org/licenses/LICENSE-2.0). 924 | 925 | [support@stormpth.com]: mailto:support@stormpath.com "Email Stormpath Support" 926 | [Express.js]: http://expressjs.com/ "Express.js" 927 | [cookie-parser]: https://github.com/expressjs/cookie-parser "Cookie Parser" 928 | [body-parser]: https://github.com/expressjs/body-parser "Body Parser" 929 | [nJwt]: https://github.com/jwtk/njwt "nJWt" 930 | [stormpath]: https://stormpath.com "Stormpath" 931 | [stormpath-sdk-angular]: https://github.com/stormpath/stormpath-angular "Stormpath AngularJS SDK" 932 | [express-stormpath]: https://github.com/stormpath/express-stormpath "Stormpath" 933 | [stormpath-node-sdk]: https://github.com/stormpath/stormpath-sdk-node "Stormpath Node SDK" 934 | [stormpath-api-key-docs]: http://docs.stormpath.com/guides/api-key-management/ "Stormpath Api Key Management" 935 | [Username and Password authentication]: http://docs.stormpath.com/rest/product-guide/#authenticate-an-account "Username and Password authentication" 936 | [Api Key Authentication]: http://docs.stormpath.com/guides/api-key-management/ "Api Key Authentication" 937 | [Cross-Site Request Forgery]: https://www.owasp.org/index.php/Cross-Site_Request_Forgery "Cross-Site Request Forgery (CSRF)" 938 | [trust-proxy-option]: http://expressjs.com/4x/api.html#trust.proxy.options.table 939 | [express.req.protocol]: http://expressjs.com/4x/api.html#req.protocol 940 | [UPGRADING.md]: UPGRADING.md 941 | -------------------------------------------------------------------------------- /UPGRADING.md: -------------------------------------------------------------------------------- 1 | # Upgrading to `express-stormath` 2 | 3 | This document explains how to migrate your application from the `stormpath-sdk-express` 4 | module to the [`express-stormpath`][] module. 5 | 6 | Please see the 7 | [Express-Stormpath Documentation](http://docs.stormpath.com/nodejs/express/latest/) 8 | for the latest documentation of the new library. 9 | 10 | ### Environment variables 11 | 12 | The format of our environment variables has changed. If you are using environment 13 | variables to pass your Stormpath configuration to your application, you will 14 | need to update the values accordingly: 15 | 16 | | Old Name | New Name | 17 | | ------------------------ | -------------------------------| 18 | | STORMPATH_API_KEY_ID | STORMPATH_CLIENT_APIKEY_ID | 19 | | STORMPATH_API_KEY_SECRET | STORMPATH_CLIENT_APIKEY_SECRET | 20 | | STORMPATH_APP_HREF | STORMPATH_APPLICATION_HREF | 21 | 22 | ### Initialization 23 | 24 | Previously the middleware was constructed, and then passed your Stormpath application, 25 | like this: 26 | 27 | ```javascript 28 | var spMiddleware = stormpathExpressSdk.createMiddleware(spConfig); 29 | 30 | spMiddleware.attachDefaults(app); 31 | ``` 32 | 33 | With `express-stormpath` the initialization now looks like this: 34 | 35 | ```javascript 36 | app.use(stormpath.init(app, { 37 | web: { 38 | spa: { 39 | enabled: true, 40 | view: path.join(__dirname, 'public', 'index.html') // the path to your Angular index.html 41 | } 42 | } 43 | })); 44 | ``` 45 | 46 | See 47 | [Configuration](http://docs.stormpath.com/nodejs/express/latest/configuration.html). 48 | 49 | ### Login Changes 50 | 51 | The new way to login a user is to make a POST to `/login`, with the fields 52 | `username` and `password`. The POST can be JSON or form encoded. See 53 | [Login](http://docs.stormpath.com/nodejs/express/latest/login.html) 54 | 55 | ### Registration Changes 56 | 57 | New user data should now be posed to `/register` as JSON or form-encoded. The 58 | new library has a rich engine for customizing the login form, please see 59 | the [Registration](http://docs.stormpath.com/nodejs/express/latest/registration.html) 60 | documentation 61 | 62 | ### Email verification 63 | 64 | To request an email verification token, POST the `email` field to `/verify`. 65 | 66 | To verify and consume the email verification token, make a GET request to 67 | `/verify?sptoken=`. 68 | 69 | ### Password Reset 70 | 71 | To request a password reset token, POST the `email` field to `/forgot`. 72 | 73 | To verify a password reset token, make a GET request to `/change?sptoken=token` 74 | 75 | To consume a password reset token, and save a new password, post the 76 | `password` and `sptoken` fields to `/change`. 77 | 78 | 79 | ### Current user 80 | 81 | To get a JSON representation of the currently authenticated user, make a GET 82 | request to `/me`. 83 | 84 | ### Forcing Authentication 85 | 86 | Previously, you would use the `authenticate` middleware like this: 87 | 88 | ```javascript 89 | app.get('/api/*',spMiddleware.authenticate,function(req,res,next){ 90 | // If we get here, the user has been authenticated 91 | // The account object is available at req.user 92 | }); 93 | ``` 94 | 95 | Now there are two options. 96 | 97 | If you are building a traditional web app or single-page app (Angular) that can use cookies, then you 98 | want to use `stormpath.loginRequired` 99 | 100 | 101 | ```javascript 102 | app.get('/protected',stormpath.loginRequired,function(req,res,next){ 103 | // If we get here, the user has been authenticated 104 | // The account object is available at req.user 105 | }); 106 | ``` 107 | 108 | If you are building an API service that only needs to use client credential and 109 | bearer authentication, use `stormpath.apiAuthenticationRequired` 110 | 111 | 112 | ```javascript 113 | app.get('/api/*',stormpath.apiAuthenticationRequired,function(req,res,next){ 114 | // If we get here, the user has been authenticated 115 | // The account object is available at req.user 116 | }); 117 | ``` 118 | 119 | ### Angular SDK Upgrade required 120 | 121 | When moving to [`express-stormpath`][], you will need to upgrade the [Stormpath Angular SDK][] to version 1.0.0 or greater. This upgrade should not affect your existing Anagular application, as the changes are internal to the library and how it communicates with the [`express-stormpath`][] backend. 122 | 123 | [`express-stormpath`]: https://github.com/stormpath/express-stormpath 124 | [Stormpath Angular SDK]: https://github.com/stormpath/stormpath-sdk-angularjs 125 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fs = require('fs'); 4 | var path = require('path'); 5 | var url = require('url'); 6 | 7 | var MiddlewareContext = require('./lib/MiddlewareContext'); 8 | 9 | /* 10 | middlewareFns is a map of all the exported functions that 11 | exist as files in the middlware/ folder 12 | */ 13 | 14 | var middlewareFns = fs.readdirSync(path.join(__dirname,'lib','middleware')) 15 | .filter(function(filename){ 16 | return (/\.js/).test(filename); 17 | }) 18 | .reduce(function(map,filename){ 19 | var fnName = filename.replace(/.js/,''); 20 | map[fnName]=require(path.join(__dirname,'lib','middleware',fnName)); 21 | return map; 22 | },{}); 23 | 24 | function autoRouterHandler(context,req,res,next){ 25 | var spConfig = context.spConfig; 26 | function _next(){ 27 | if(url.parse(req.url).pathname===spConfig.tokenEndpoint){ 28 | if(req.method === 'POST'){ 29 | context.authenticateForToken(req,res,next); 30 | }else{ 31 | res.status(405).end(); 32 | } 33 | }else if(url.parse(req.url).pathname===spConfig.logoutEndpoint){ 34 | context.logout(req,res,next); 35 | }else{ 36 | context.authenticate(req,res,next); 37 | } 38 | } 39 | 40 | if(spConfig.allowedOrigins.length>0){ 41 | context.corsHandler(req,res,_next); 42 | }else{ 43 | _next(); 44 | } 45 | 46 | } 47 | 48 | function createMiddleware(spConfig) { 49 | 50 | /* 51 | The middleware context is where we maintain everything 52 | that Stormpath needs to get work done with this module. 53 | It has a Stormpath Client, bound to a Stormpath application. 54 | It retains the given config, as well as the properties 55 | file that defines all the strings for error messages and 56 | user messages. 57 | */ 58 | spConfig = typeof spConfig === 'object' ? spConfig : {}; 59 | var context = new MiddlewareContext(spConfig); 60 | var boundMiddleware = {}; 61 | 62 | /* 63 | For each exported middleware function, create a bound version 64 | which is bound to the middleware context 65 | */ 66 | 67 | Object.keys(middlewareFns).reduce(function(boundMiddleware,fnName){ 68 | boundMiddleware[fnName] = middlewareFns[fnName].bind(context); 69 | return boundMiddleware; 70 | },boundMiddleware); 71 | 72 | /* 73 | For all middleware functions, we want to do CORS filtering 74 | first (do we need to add cors handlers)? 75 | */ 76 | 77 | Object.keys(boundMiddleware).forEach(function(fnName){ 78 | var fn = boundMiddleware[fnName]; 79 | if(fnName!=='corsHandler' && fnName!=='groupsRequired' && fnName!=='groupRequired'){ 80 | boundMiddleware[fnName] = function corsPrefilter(req,res,next){ 81 | boundMiddleware.corsHandler(req,res,fn.bind(context,req,res,next)); 82 | }; 83 | } 84 | }); 85 | 86 | /* 87 | Attach each bound function to the middleware context, using the 88 | function name as the property. The middleware context then has 89 | access to these named functions, so that it can delegate requests 90 | to them. 91 | */ 92 | 93 | Object.keys(boundMiddleware).forEach(function(fnName){ 94 | context[fnName] = boundMiddleware[fnName]; 95 | }); 96 | 97 | /* 98 | We pass along the bound middleware to the "auto router", the 99 | "auto router" is the function that is passed to app.use() 100 | if you use the statement app.use(stormpathMiddlweare) 101 | */ 102 | 103 | var autoRouter = autoRouterHandler.bind(null,context); 104 | Object.keys(boundMiddleware).reduce(function(autoRouter,fn){ 105 | autoRouter[fn]=boundMiddleware[fn]; 106 | return autoRouter; 107 | },autoRouter); 108 | autoRouter.getApplication = context.getApplication; 109 | autoRouter.spClient = context.spClient; 110 | 111 | /* 112 | attachDefaults is used to manually bind middleware to 113 | specific endpoints and methods, rather than a single middleware 114 | that you use with the autoRouter 115 | */ 116 | 117 | autoRouter.attachDefaults = function(app){ 118 | app.set('stormpathMiddleware',context); 119 | app.get(context.spConfig.currentUserEndpoint, 120 | context.authenticate.bind(context), 121 | context.currentUser.bind(context) 122 | ); 123 | app.get(context.spConfig.logoutEndpoint,boundMiddleware.logout); 124 | app.post(context.spConfig.userCollectionEndpoint,boundMiddleware.register); 125 | app.post(context.spConfig.tokenEndpoint,boundMiddleware.authenticateForToken); 126 | app.options(context.spConfig.tokenEndpoint,boundMiddleware.authenticateForToken); 127 | app.post(context.spConfig.resendEmailVerificationEndpoint,boundMiddleware.resendEmailVerification); 128 | app.options(context.spConfig.resendEmailVerificationEndpoint,boundMiddleware.resendEmailVerification); 129 | app.post(context.spConfig.emailVerificationTokenCollectionEndpoint,boundMiddleware.verifyEmailVerificationToken); 130 | app.options(context.spConfig.emailVerificationTokenCollectionEndpoint,boundMiddleware.verifyEmailVerificationToken); 131 | app.get(context.spConfig.passwordResetTokenCollectionEndpoint +'/:sptoken?',boundMiddleware.passwordReset); 132 | app.post(context.spConfig.passwordResetTokenCollectionEndpoint +'/:sptoken?',boundMiddleware.passwordReset); 133 | app.options(context.spConfig.passwordResetTokenCollectionEndpoint +'/:sptoken?',boundMiddleware.passwordReset); 134 | }; 135 | 136 | return autoRouter; 137 | } 138 | 139 | 140 | var exports = { 141 | createMiddleware: createMiddleware 142 | }; 143 | 144 | Object.keys(middlewareFns).forEach(function(fnName){ 145 | var fn = middlewareFns[fnName]; 146 | var constructorName = fnName.charAt(0).toUpperCase() + fnName.slice(1); 147 | exports[constructorName] = function(spConfig){ 148 | spConfig = typeof spConfig === 'object' ? spConfig : {}; 149 | var context = new MiddlewareContext(spConfig); 150 | return fn.bind(context); 151 | }; 152 | }); 153 | 154 | module.exports = exports; 155 | -------------------------------------------------------------------------------- /lib/MiddlewareContext.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var nJwt = require('njwt'); 4 | var uuid = require('node-uuid'); 5 | var stormpath = require('stormpath'); 6 | 7 | var pkg = require('../package.json'); 8 | var properties = require('../properties.json'); 9 | var writeToken = require('./middleware/writeToken'); 10 | 11 | function isNumeric(n) { 12 | return !isNaN(parseFloat(n)) && isFinite(n); 13 | } 14 | 15 | function MiddlewareContext(spConfig){ 16 | var self = this; 17 | this.spConfig = typeof spConfig === 'object' ? spConfig : {}; 18 | this.stormpath = spConfig.stormpath || stormpath; 19 | var spApplication, spTenant; 20 | this.jwtLib = spConfig.jwtLib || nJwt; 21 | this.getUuid = spConfig.uuidGenerator || uuid; 22 | this.properties = spConfig.properties || properties; 23 | this.scopeFactory = spConfig.scopeFactory || undefined; 24 | spConfig.xsrf = spConfig.xsrf === false ? false : true; 25 | spConfig.forceHttps = spConfig.forceHttps === true ? true : false; 26 | spConfig.writeTokens = spConfig.writeTokens === false ? false : true; 27 | spConfig.endOnError = spConfig.endOnError === false ? false : true; 28 | spConfig.logoutEndpoint = spConfig.logoutEndpoint || this.properties.configuration.DEFAULT_LOGOUT_ENDPOINT; 29 | spConfig.tokenEndpoint = spConfig.tokenEndpoint || this.properties.configuration.DEFAULT_TOKEN_ENDPOINT; 30 | spConfig.userCollectionEndpoint = spConfig.userCollectionEndpoint || this.properties.configuration.DEFAULT_USER_COLLECTION_ENDPOINT; 31 | spConfig.currentUserEndpoint = spConfig.currentUserEndpoint || this.properties.configuration.DEFAULT_CURRENT_USER_ENDPOINT; 32 | spConfig.resendEmailVerificationEndpoint = spConfig.resendEmailVerificationEndpoint || this.properties.configuration.RESEND_EMAIL_VERIFICATION_ENDPOINT; 33 | spConfig.emailVerificationTokenCollectionEndpoint = spConfig.emailVerificationTokenCollectionEndpoint || this.properties.configuration.EMAIL_VERIFICATION_TOKEN_COLLECTION_ENDPOINT; 34 | spConfig.passwordResetTokenCollectionEndpoint = spConfig.passwordResetTokenCollectionEndpoint || this.properties.configuration.PASSWORD_RESET_TOKEN_COLLECTION_ENDPOINT; 35 | spConfig.allowedOrigins = spConfig.allowedOrigins || []; 36 | spConfig.accessTokenTtl = isNumeric(spConfig.accessTokenTtl) ? spConfig.accessTokenTtl : 3600; 37 | spConfig.accessTokenCookieName = spConfig.accessTokenCookieName || this.properties.configuration.DEFAULT_ACCESS_TOKEN_COOKIE_NAME; 38 | spConfig.writeAccessTokenToCookie = spConfig.writeAccessTokenToCookie === false ? false : true; 39 | spConfig.writeAccessTokenResponse = spConfig.writeAccessTokenResponse === true ? true : false; 40 | 41 | spConfig.apiKeyId = spConfig.apiKeyId || process.env.STORMPATH_API_KEY_ID; 42 | spConfig.apiKeySecret = spConfig.apiKeySecret || process.env.STORMPATH_API_KEY_SECRET; 43 | spConfig.appHref = spConfig.appHref || process.env.STORMPATH_APP_HREF; 44 | 45 | this.postRegistrationHandler = (function(){ 46 | var rh = spConfig.postRegistrationHandler; 47 | if(rh){ 48 | if(typeof rh==='function'){ 49 | return rh; 50 | }else{ 51 | throw new Error('postRegistrationHandler must be a function'); 52 | } 53 | } 54 | return null; 55 | })(); 56 | 57 | if(spConfig.spClient){ 58 | this.spClient = spConfig.spClient; 59 | }else if(spConfig.apiKeyId && spConfig.apiKeySecret && spConfig.appHref){ 60 | this.spClient = new this.stormpath.Client({ 61 | apiKey: new this.stormpath.ApiKey(this.spConfig.apiKeyId,this.spConfig.apiKeySecret), 62 | userAgent: 'stormpath-sdk-express/' + pkg.version 63 | }); 64 | }else{ 65 | if(!spConfig.apiKeyId){ 66 | throw new Error(this.properties.errors.MISSING_API_KEY_ID); 67 | } 68 | else if(!spConfig.apiKeySecret){ 69 | throw new Error(this.properties.errors.MISSING_API_KEY_SECRET); 70 | }else if(!spConfig.appHref){ 71 | throw new Error(this.properties.errors.MISSING_APP_HREF); 72 | }else{ 73 | throw new Error(this.properties.errors.INVALID_SP_CONFIG); 74 | } 75 | } 76 | 77 | this.spClient.getApplication(this.spConfig.appHref,function(err,application){ 78 | if(err){ 79 | console.error(err); 80 | }else{ 81 | spApplication = application; 82 | } 83 | }); 84 | this.spClient.getCurrentTenant(function(err,tenant){ 85 | if(err){ 86 | console.error(err); 87 | }else{ 88 | spTenant = tenant; 89 | } 90 | }); 91 | this.tokenWriter = writeToken.bind(this); 92 | this.getApplication = function(){ 93 | return spApplication; 94 | }; 95 | this.getTenant = function(){ 96 | return spTenant; 97 | }; 98 | this.handleApplicationError = function(errMsgString,res){ 99 | res.status(500).json({errorMessage: errMsgString}); 100 | }; 101 | this.handleSdkError = function(err,res){ 102 | res.status(err.status || err.statusCode || 500) 103 | .json({ 104 | code: err.code, 105 | errorMessage: err.userMessage || err.developerMessage || 106 | self.properties.errors.library.NODE_SDK_ERROR 107 | }); 108 | }; 109 | this.handleAuthenticationError = function(err,req,res,next){ 110 | if(self.spConfig.endOnError){ 111 | if(req.cookies && req.cookies[self.spConfig.accessTokenCookieName]){ 112 | res.setHeader('set-cookie',self.spConfig.accessTokenCookieName+'=delete;path=/;Expires='+new Date().toUTCString()); 113 | } 114 | res.status(err.status || err.statusCode || 401) 115 | .json({ 116 | code: err.code, 117 | errorMessage: err.userMessage || 118 | self.properties.errors.authentication.UNKNOWN 119 | }); 120 | }else{ 121 | req.authenticationError = err; 122 | next(); 123 | } 124 | }; 125 | this.handleAuthorizationError = function(err,req,res,next){ 126 | if(self.spConfig.endOnError){ 127 | res.status(err.status || err.statusCode || 403) 128 | .json({ 129 | code: err.code, 130 | errorMessage: err.userMessage || 131 | self.properties.errors.authorization.FORBIDDEN 132 | }); 133 | }else{ 134 | req.authenticationError = err; 135 | next(); 136 | } 137 | }; 138 | this.xsrfValidator = function xsrfValidator(req,res,next){ 139 | if(this.spConfig.xsrf && req.method==='POST'){ 140 | var token = req.headers['x-xsrf-token'] || (req.body && req.body.xsrfToken); 141 | if(token===req.accessToken.body.xsrfToken){ 142 | next(); 143 | }else{ 144 | this.handleAuthenticationError({ 145 | userMessage:this.properties.errors.xsrf.XSRF_MISMATCH 146 | },req,res,next); 147 | } 148 | }else{ 149 | next(); 150 | } 151 | }; 152 | this.jwsClaimsParser = 153 | this.jwtLib.Parser().setSigningKey(this.spConfig.apiKeySecret); 154 | return this; 155 | } 156 | 157 | module.exports = MiddlewareContext; -------------------------------------------------------------------------------- /lib/middleware/authenticate.js: -------------------------------------------------------------------------------- 1 | 2 | function authenticate(req,res,next){ 3 | var context = this; 4 | var spConfig = context.spConfig; 5 | var properties = context.properties; 6 | var authHeader = req.headers.Authorization || req.headers.authorization; 7 | var authCookie = req.cookies && req.cookies[spConfig.accessTokenCookieName]; 8 | if( authHeader && authHeader.match(/Bearer/)){ 9 | context.authenticateBearerAuthorizationHeader(req,res,context.xsrfValidator.bind(context,req,res,next)); 10 | }else if(authCookie){ 11 | context.authenticateCookie(req,res,context.xsrfValidator.bind(context,req,res,next)); 12 | }else if(authHeader && authHeader.match(/Basic/)){ 13 | context.authenticateBasicAuth(req,res,context.xsrfValidator.bind(context,req,res,next)); 14 | }else{ 15 | context.handleAuthenticationError({ 16 | userMessage:properties.errors.authentication.UNAUTHENTICATED 17 | },req,res,next); 18 | } 19 | 20 | } 21 | 22 | module.exports = authenticate; -------------------------------------------------------------------------------- /lib/middleware/authenticateApiKeyForToken.js: -------------------------------------------------------------------------------- 1 | var url = require('url'); 2 | 3 | function authenticateApiKeyForToken(req,res,next){ 4 | var context = this; 5 | var properties = context.properties; 6 | var scopeFactory = context.scopeFactory; 7 | var spApplication = context.getApplication(); 8 | var urlParams = url.parse(req.url,true).query; 9 | var requestedScope = (req.body && req.body.scope) || urlParams.scope || ''; 10 | 11 | if(!spApplication){ 12 | return context.handleApplicationError(properties.errors.library.SP_APP_UNINITIALIZED,res); 13 | } 14 | spApplication.authenticateApiRequest({request: req}, function(err,authenticationResult){ 15 | if(err){ 16 | context.handleAuthenticationError(err,req,res,next); 17 | }else{ 18 | req.authenticationResult = authenticationResult; 19 | 20 | authenticationResult.getAccount(function(err,account){ 21 | if(err){ 22 | context.handleAuthenticationError(err,req,res,next); 23 | }else{ 24 | req.user = account; 25 | req.jwt = authenticationResult.getJwt().setTtl(context.spConfig.accessTokenTtl); 26 | if(context.spConfig.writeTokens && scopeFactory){ 27 | 28 | scopeFactory(req,res,authenticationResult,account,requestedScope,function(err,scope){ 29 | if(err){ 30 | context.handleAuthenticationError(err,req,res,next); 31 | }else if(scope){ 32 | req.jwt.body.scope = scope; 33 | res.status(200).json(req.authenticationResult.getAccessTokenResponse(req.jwt)); 34 | }else{ 35 | res.status(200).json(req.authenticationResult.getAccessTokenResponse(req.jwt)); 36 | } 37 | }); 38 | 39 | }else if(context.spConfig.writeTokens){ 40 | res.status(200).json(req.authenticationResult.getAccessTokenResponse(req.jwt)); 41 | }else{ 42 | next(); 43 | } 44 | } 45 | }); 46 | 47 | } 48 | }); 49 | } 50 | 51 | module.exports = authenticateApiKeyForToken; -------------------------------------------------------------------------------- /lib/middleware/authenticateBasicAuth.js: -------------------------------------------------------------------------------- 1 | function authenticateBasicAuth(req,res,next){ 2 | var context = this; 3 | var spApplication = context.getApplication(); 4 | var properties = context.properties; 5 | if(!spApplication){ 6 | return context.handleApplicationError(properties.errors.library.SP_APP_UNINITIALIZED,res); 7 | } 8 | spApplication.authenticateApiRequest({ 9 | request: req 10 | },function(err,authenticationResult){ 11 | req.authenticationResult = authenticationResult; 12 | if(err){ 13 | context.handleAuthenticationError(err,req,res,next); 14 | }else{ 15 | authenticationResult.getAccount(function(err,account){ 16 | if(err){ 17 | context.handleAuthenticationError(err,req,res,next); 18 | }else{ 19 | req.user = account; 20 | next(); 21 | } 22 | }); 23 | } 24 | 25 | }); 26 | } 27 | 28 | module.exports = authenticateBasicAuth; -------------------------------------------------------------------------------- /lib/middleware/authenticateBearerAuthorizationHeader.js: -------------------------------------------------------------------------------- 1 | var jwtErrors = require('njwt/properties.json').errors; 2 | 3 | function authenticateBearerAuthorizationHeader(req,res,next){ 4 | var context = this; 5 | var properties = context.properties; 6 | 7 | var spApplication = context.getApplication(); 8 | 9 | if(!spApplication){ 10 | return context.handleApplicationError(properties.errors.library.SP_APP_UNINITIALIZED,res); 11 | } 12 | 13 | var accessToken = (req.headers.authorization || '').replace(/Bearer /i,''); 14 | 15 | if(!accessToken){ 16 | return context.handleAuthenticationError({ 17 | userMessage:properties.errors.authentication.UNAUTHENTICATED 18 | },req,res,next); 19 | } 20 | 21 | context.jwsClaimsParser.parseClaimsJws(accessToken,function(err,jwt){ 22 | if(err){ 23 | if(err.userMessage===jwtErrors.PARSE_ERROR){ 24 | err.statusCode = 400; 25 | } 26 | context.handleAuthenticationError(err,req,res,next); 27 | }else{ 28 | req.accessToken = jwt; 29 | spApplication.authenticateApiRequest({ 30 | request: req 31 | },function(err,authenticationResult){ 32 | req.authenticationResult = authenticationResult; 33 | if(err){ 34 | context.handleAuthenticationError(err,req,res,next); 35 | }else{ 36 | authenticationResult.getAccount(function(err,account){ 37 | if(err){ 38 | context.handleAuthenticationError(err,req,res,next); 39 | }else{ 40 | req.user = account; 41 | next(); 42 | } 43 | }); 44 | } 45 | 46 | }); 47 | } 48 | }); 49 | } 50 | module.exports = authenticateBearerAuthorizationHeader; -------------------------------------------------------------------------------- /lib/middleware/authenticateCookie.js: -------------------------------------------------------------------------------- 1 | function authenticateCookie(req,res,next){ 2 | var context = this; 3 | var properties = context.properties; 4 | var spClient = context.spClient; 5 | var spConfig = context.spConfig; 6 | 7 | if(!req.cookies || (typeof req.cookies !== 'object')){ 8 | return context.handleApplicationError(properties.errors.MISSING_COOKIE_MIDDLEWARE,res); 9 | } 10 | 11 | var accessToken = req.cookies[spConfig.accessTokenCookieName]; 12 | 13 | if(!accessToken){ 14 | return context.handleAuthenticationError({ 15 | status: 400, 16 | userMessage:properties.errors.authentication.UNAUTHENTICATED 17 | },req,res,next); 18 | } 19 | 20 | context.jwsClaimsParser.parseClaimsJws(accessToken,function(err,jwt){ 21 | 22 | if(err){ 23 | context.handleAuthenticationError({ 24 | status: 400, 25 | userMessage: err.userMessage 26 | },req,res,next); 27 | }else{ 28 | req.accessToken = jwt; 29 | spClient.getAccount(jwt.body.sub,function(err,account){ 30 | if(err){ 31 | context.handleAuthenticationError(err,req,res,next); 32 | }else if(account.status==='ENABLED'){ 33 | req.user = account; 34 | next(); 35 | }else{ 36 | context.handleAuthenticationError({ 37 | userMessage:properties.errors.authentication.UNAUTHENTICATED 38 | },req,res,next); 39 | } 40 | }); 41 | } 42 | }); 43 | 44 | } 45 | 46 | module.exports = authenticateCookie; -------------------------------------------------------------------------------- /lib/middleware/authenticateForToken.js: -------------------------------------------------------------------------------- 1 | var url = require('url'); 2 | 3 | function authenticateForToken(req,res, next){ 4 | var context = this; 5 | var properties = context.properties; 6 | 7 | var urlParams = url.parse(req.url,true).query; 8 | 9 | if(urlParams.grant_type && urlParams.grant_type==='password'){ 10 | context.authenticateUsernamePasswordForToken(req,res,next); 11 | }else if(urlParams.grant_type && urlParams.grant_type==='social'){ 12 | context.authenticateSocialForToken(req,res,next); 13 | }else if(urlParams.grant_type && urlParams.grant_type==='client_credentials'){ 14 | context.authenticateApiKeyForToken(req,res,next); 15 | }else{ 16 | context.handleAuthenticationError({ 17 | userMessage:properties.errors.authentication.UNSUPPORTED_GRANT_TYPE 18 | },req,res,next); 19 | } 20 | } 21 | 22 | module.exports = authenticateForToken; -------------------------------------------------------------------------------- /lib/middleware/authenticateSocialForToken.js: -------------------------------------------------------------------------------- 1 | var url = require('url'); 2 | 3 | function authenticateSocialForToken(req,res, next){ 4 | var context = this; 5 | var properties = context.properties; 6 | var spApplication = context.getApplication(); 7 | 8 | var tokenWriter = context.tokenWriter; 9 | var scopeFactory = context.scopeFactory; 10 | 11 | var urlParams = url.parse(req.url,true).query; 12 | var requestedScope = (req.body && req.body.scope) || urlParams.scope || ''; 13 | if(!spApplication){ 14 | return context.handleApplicationError(properties.errors.library.SP_APP_UNINITIALIZED,res); 15 | } 16 | else if(req.body && req.body.providerId && req.body.accessToken) { 17 | spApplication.getAccount({ 18 | providerData: { 19 | providerId: req.body.providerId, 20 | accessToken: req.body.accessToken 21 | } 22 | }, function (err, accountResult) { 23 | if (err) { 24 | context.handleAuthenticationError(err, req, res, next); 25 | } else { 26 | // writeTokens expects an authenticationResult, so we'll act like one 27 | req.authenticationResult = accountResult; 28 | req.authenticationResult.getAccessTokenResponse = function getAccessTokenResponse(jwt) { 29 | jwt = jwt || this.getJwt(); 30 | var resp = { 31 | 'access_token': jwt.compact(), 32 | 'token_type': 'Bearer', 33 | 'expires_in': jwt.ttl 34 | }; 35 | if (jwt.body.scope) { 36 | resp.scope = jwt.body.scope; 37 | } 38 | return resp; 39 | }; 40 | req.user = accountResult.account; 41 | req.jwt = context.jwtLib.Jwt({ 42 | iss: context.spConfig.appHref, 43 | sub: accountResult.account.href, 44 | jti: context.getUuid() 45 | }).signWith('HS256',context.spConfig.apiKeySecret).setTtl(3600); 46 | 47 | if (context.spConfig.writeTokens && scopeFactory) { 48 | scopeFactory(req, res, accountResult, accountResult.account, requestedScope, 49 | function (err, scope) { 50 | if (err) { 51 | context.handleAuthenticationError(err, req, res, next); 52 | } else if (scope) { 53 | req.jwt.body.scope = scope; 54 | tokenWriter(req, res); 55 | } else { 56 | tokenWriter(req, res); 57 | } 58 | }); 59 | } else if (context.spConfig.writeTokens) { 60 | tokenWriter(req, res); 61 | } else { 62 | next(); 63 | } 64 | } 65 | }); 66 | }else{ 67 | context.handleAuthenticationError({ 68 | status: 400, 69 | userMessage:properties.errors.authentication.BAD_ACCESS_TOKEN_BODY 70 | },req,res,next); 71 | } 72 | } 73 | 74 | module.exports = authenticateSocialForToken; -------------------------------------------------------------------------------- /lib/middleware/authenticateUsernamePasswordForToken.js: -------------------------------------------------------------------------------- 1 | var url = require('url'); 2 | 3 | function authenticateUsernamePasswordForToken(req,res, next){ 4 | var context = this; 5 | var properties = context.properties; 6 | var spApplication = context.getApplication(); 7 | 8 | var tokenWriter = context.tokenWriter; 9 | var scopeFactory = context.scopeFactory; 10 | 11 | var urlParams = url.parse(req.url,true).query; 12 | var requestedScope = (req.body && req.body.scope) || urlParams.scope || ''; 13 | if(!spApplication){ 14 | return context.handleApplicationError(properties.errors.library.SP_APP_UNINITIALIZED,res); 15 | } 16 | else if(req.body && req.body.username && req.body.password){ 17 | spApplication.authenticateAccount({username:req.body.username,password:req.body.password},function(err,authenticationResult){ 18 | if(err){ 19 | if(err.userMessage==='Invalid username or password.'){ 20 | err.status = 401; 21 | } 22 | context.handleAuthenticationError(err,req,res,next); 23 | }else{ 24 | req.authenticationResult = authenticationResult; 25 | authenticationResult.getAccount({expand:'groups,customData'},function(err,account){ 26 | if(err){ 27 | context.handleAuthenticationError(err,req,res,next); 28 | }else{ 29 | req.user = account; 30 | req.jwt = authenticationResult.getJwt().setTtl(context.spConfig.accessTokenTtl); 31 | if(context.spConfig.writeTokens && scopeFactory){ 32 | 33 | scopeFactory(req,res,authenticationResult,account,requestedScope,function(err,scope){ 34 | if(err){ 35 | context.handleAuthenticationError(err,req,res,next); 36 | }else if(scope){ 37 | req.jwt.body.scope = scope; 38 | tokenWriter(req,res); 39 | }else{ 40 | tokenWriter(req,res); 41 | } 42 | }); 43 | 44 | }else if(context.spConfig.writeTokens){ 45 | tokenWriter(req,res); 46 | }else{ 47 | next(); 48 | } 49 | } 50 | }); 51 | 52 | } 53 | }); 54 | }else{ 55 | context.handleAuthenticationError({ 56 | status: 400, 57 | userMessage:properties.errors.authentication.BAD_PASSWORD_BODY 58 | },req,res,next); 59 | } 60 | } 61 | 62 | module.exports = authenticateUsernamePasswordForToken; -------------------------------------------------------------------------------- /lib/middleware/corsHandler.js: -------------------------------------------------------------------------------- 1 | function corsHandler(req,res,next) { 2 | var context = this; 3 | var spConfig = context.spConfig; 4 | if(req.method === 'OPTIONS' && spConfig.allowedOrigins.indexOf(req.headers.origin)>-1){ 5 | res.setHeader('Access-Control-Allow-Origin',req.headers.origin); 6 | res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); 7 | res.setHeader('Access-Control-Allow-Credentials','true'); 8 | return res.status(200).end(); 9 | }else if((req.headers.host!==req.headers.origin)){ 10 | if(spConfig.allowedOrigins.indexOf(req.headers.origin)>-1){ 11 | res.setHeader('Access-Control-Allow-Origin',req.headers.origin); 12 | res.setHeader('Access-Control-Allow-Credentials','true'); 13 | } 14 | } 15 | next(); 16 | } 17 | module.exports = corsHandler; -------------------------------------------------------------------------------- /lib/middleware/currentUser.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var async = require('async'); 4 | 5 | function currentUser(req,res){ 6 | var context = this; 7 | async.parallel({ 8 | groupsCollection: req.user.getGroups.bind(req.user), 9 | customData: req.user.getCustomData.bind(req.user) 10 | },function result(err,results) { 11 | if(err){ 12 | context.handleSdkError(err,res); 13 | }else{ 14 | req.user.groups = results.groupsCollection.items; 15 | req.user.customData = results.customData; 16 | res.json(req.user); 17 | } 18 | }); 19 | 20 | } 21 | 22 | module.exports = currentUser; -------------------------------------------------------------------------------- /lib/middleware/groupRequired.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('underscore'); 4 | 5 | function assertAGroup(context,list,req,res,next) { 6 | 7 | req.user.getGroups(function(err,groupsCollection) { 8 | if(err){ 9 | context.handleSdkError(err,res); 10 | }else{ 11 | if(_.intersection(list,_.pluck(groupsCollection.items,'name')).length > 0){ 12 | next(); 13 | }else{ 14 | context.handleAuthorizationError({},req,res,next); 15 | } 16 | } 17 | }); 18 | } 19 | 20 | function aGroupRequired(groupFilter){ 21 | var context = this; 22 | var list = Array.isArray(groupFilter) ? groupFilter : ( typeof groupFilter === 'string' ? [groupFilter] : []); 23 | return function (req,res,next) { 24 | if(req.user){ 25 | assertAGroup(context,list,req,res,next); 26 | }else{ 27 | context.authenticate(req,res,function(req,res) { 28 | assertAGroup(context,list,req,res,next); 29 | }.bind(context,req,res)); 30 | } 31 | 32 | }; 33 | 34 | } 35 | 36 | module.exports = aGroupRequired; -------------------------------------------------------------------------------- /lib/middleware/groupsRequired.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _ = require('underscore'); 4 | 5 | function assertGroups(context,list,req,res,next) { 6 | 7 | req.user.getGroups(function(err,groupsCollection) { 8 | if(err){ 9 | context.handleSdkError(err,res); 10 | }else{ 11 | if(list.length === _.intersection(list,_.pluck(groupsCollection.items,'name')).length){ 12 | next(); 13 | }else{ 14 | context.handleAuthorizationError({},req,res,next); 15 | } 16 | } 17 | }); 18 | } 19 | 20 | function groupsRequired(groupFilter){ 21 | var context = this; 22 | var list = Array.isArray(groupFilter) ? groupFilter : ( typeof groupFilter === 'string' ? [groupFilter] : []); 23 | return function (req,res,next) { 24 | if(req.user){ 25 | assertGroups(context,list,req,res,next); 26 | }else{ 27 | context.authenticate(req,res,function(req,res) { 28 | assertGroups(context,list,req,res,next); 29 | }.bind(context,req,res)); 30 | } 31 | 32 | }; 33 | 34 | } 35 | 36 | module.exports = groupsRequired; -------------------------------------------------------------------------------- /lib/middleware/logout.js: -------------------------------------------------------------------------------- 1 | function logout(req,res) { 2 | var context = this; 3 | var spConfig = context.spConfig; 4 | var now = new Date().toUTCString(); 5 | var existing = (res.getHeader('Set-Cookie') || res.getHeader('set-cookie') || '') ; 6 | var cookies = existing ? [existing] : []; 7 | if(req.cookies && req.cookies['XSRF-TOKEN']){ 8 | cookies.push('XSRF-TOKEN=delete; Expires='+now+';Path=/;'); 9 | } 10 | if(req.cookies && req.cookies[spConfig.accessTokenCookieName]){ 11 | cookies.push( 12 | spConfig.accessTokenCookieName+'=delete; Expires='+new Date().toUTCString()+'; '+ 13 | ((spConfig.forceHttps || (req.protocol==='https'))?'Secure; ':'')+'HttpOnly;Path=/;' 14 | ); 15 | } 16 | 17 | if(cookies.length){ 18 | res.setHeader('set-cookie',cookies); 19 | } 20 | 21 | res.statusCode = 204; 22 | res.end(); 23 | 24 | } 25 | 26 | module.exports = logout; -------------------------------------------------------------------------------- /lib/middleware/passwordReset.js: -------------------------------------------------------------------------------- 1 | 2 | function passwordReset(req,res,next){ 3 | var context = this; 4 | var properties = context.properties; 5 | var spApplication = context.getApplication(); 6 | 7 | if(!spApplication){ 8 | return context.handleApplicationError(properties.errors.library.SP_APP_UNINITIALIZED,res); 9 | } 10 | if(req.params.sptoken && req.body.password){ 11 | spApplication.resetPassword(req.params.sptoken,req.body.password,function(err){ 12 | if(err){ 13 | context.handleAuthenticationError(err,req,res,next); 14 | }else{ 15 | res.status(200).end(); 16 | } 17 | }); 18 | } 19 | else if(req.params.sptoken){ 20 | spApplication.verifyPasswordResetToken(req.params.sptoken,function(err){ 21 | if(err){ 22 | context.handleAuthenticationError(err,req,res,next); 23 | }else{ 24 | res.status(200).end(); 25 | } 26 | }); 27 | }else{ 28 | spApplication.sendPasswordResetEmail(req.body.email,function(err){ 29 | if(err){ 30 | context.handleAuthenticationError(err,req,res,next); 31 | }else{ 32 | res.status(201).end(); 33 | } 34 | }); 35 | } 36 | 37 | } 38 | 39 | module.exports = passwordReset; -------------------------------------------------------------------------------- /lib/middleware/register.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function defaultResponder(req,res){ 4 | res.statusCode = req.user.status==='ENABLED' ? 201 : 202; 5 | res.end(); 6 | } 7 | 8 | function register(req,res,next){ 9 | var context = this; 10 | var properties = context.properties; 11 | var spApplication = context.getApplication(); 12 | 13 | if(!spApplication){ 14 | return context.handleApplicationError(properties.errors.library.SP_APP_UNINITIALIZED,res); 15 | } 16 | spApplication.createAccount(req.body,function(err,account){ 17 | if(err){ 18 | context.handleAuthenticationError(err,req,res,next); 19 | }else{ 20 | req.user = account; 21 | if(context.postRegistrationHandler){ 22 | context.postRegistrationHandler(req,res,defaultResponder.bind(null,req,res)); 23 | }else{ 24 | defaultResponder(req,res); 25 | } 26 | } 27 | }); 28 | 29 | } 30 | 31 | module.exports = register; -------------------------------------------------------------------------------- /lib/middleware/resendEmailVerification.js: -------------------------------------------------------------------------------- 1 | 2 | function resendVerificationEmail(req,res,next){ 3 | var context = this; 4 | var properties = context.properties; 5 | var spApplication = context.getApplication(); 6 | 7 | if(!spApplication){ 8 | return context.handleApplicationError(properties.errors.library.SP_APP_UNINITIALIZED,res); 9 | } 10 | spApplication.resendVerificationEmail(req.body,function(err){ 11 | if(err){ 12 | context.handleAuthenticationError(err,req,res,next); 13 | }else{ 14 | res.statusCode = 201; 15 | res.end(); 16 | } 17 | }); 18 | 19 | } 20 | 21 | module.exports = resendVerificationEmail; -------------------------------------------------------------------------------- /lib/middleware/verifyEmailVerificationToken.js: -------------------------------------------------------------------------------- 1 | 2 | function verifyEmailVerificationToken(req,res,next){ 3 | var context = this; 4 | var properties = context.properties; 5 | var spTenant = context.getTenant(); 6 | 7 | if(!spTenant){ 8 | return context.handleApplicationError(properties.errors.library.SP_TENANT_UNINITIALIZED,res); 9 | } 10 | 11 | spTenant.verifyAccountEmail(req.body.sptoken,function(err){ 12 | if(err){ 13 | context.handleAuthenticationError(err,req,res,next); 14 | }else{ 15 | res.statusCode = 202; 16 | res.end(); 17 | } 18 | }); 19 | 20 | } 21 | 22 | module.exports = verifyEmailVerificationToken; -------------------------------------------------------------------------------- /lib/middleware/writeToken.js: -------------------------------------------------------------------------------- 1 | 2 | function writeToken(req,res) { 3 | var context = this; 4 | var spConfig = context.spConfig; 5 | var uuid = context.getUuid; 6 | var jwt = req.jwt; 7 | if(spConfig.xsrf){ 8 | jwt.body.xsrfToken = uuid(); 9 | res.setHeader('set-cookie','XSRF-TOKEN='+jwt.body.xsrfToken+'; Expires='+new Date(jwt.body.exp*1000).toUTCString()+';Path=/;'); 10 | } 11 | if(spConfig.writeAccessTokenToCookie){ 12 | var existing = (res.getHeader('Set-Cookie') || res.getHeader('set-cookie') || '') ; 13 | res.setHeader('set-cookie', 14 | [existing, 15 | spConfig.accessTokenCookieName+'='+jwt.compact()+ 16 | '; Expires='+new Date(jwt.body.exp*1000).toUTCString()+'; '+ 17 | ((spConfig.forceHttps || (req.protocol==='https'))?'Secure; ':'')+'HttpOnly;Path=/;'] 18 | ); 19 | } 20 | 21 | if(spConfig.writeAccessTokenResponse){ 22 | res.status(200).json(req.authenticationResult.getAccessTokenResponse(jwt)); 23 | }else{ 24 | res.statusCode = 201; 25 | res.end(); 26 | } 27 | } 28 | 29 | module.exports = writeToken; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stormpath-sdk-express", 3 | "version": "0.6.0", 4 | "main": "index.js", 5 | "description": "Official Stormpath SDK for Express.js", 6 | "keywords": [ 7 | "token authentication", 8 | "stormpath", 9 | "api", 10 | "api authentication", 11 | "user management", 12 | "user login", 13 | "identity", 14 | "identity management", 15 | "account", 16 | "account login", 17 | "login", 18 | "authentication", 19 | "authorization", 20 | "access control", 21 | "password", 22 | "password hash", 23 | "express" 24 | ], 25 | "license": "Apache-2.0", 26 | "homepage": "https://github.com/stormpath/stormpath-sdk-express", 27 | "bugs": "https://github.com/stormpath/stormpath-sdk-express/issues", 28 | "author": { 29 | "name": "Stormpath, Inc.", 30 | "email": "support@stormpath.com", 31 | "url": "http://www.stormpath.com" 32 | }, 33 | "repository": { 34 | "type": "git", 35 | "url": "git://github.com/stormpath/stormpath-sdk-express.git" 36 | }, 37 | "scripts": { 38 | "test": "mocha --timeout=5000 --reporter spec --bail --check-leaks test/", 39 | "test-debug": "mocha --timeout=5000 --debug-brk --reporter spec --bail --check-leaks test/", 40 | "test-cov": "istanbul cover node_modules/mocha/bin/_mocha -- --timeout=5000 --reporter dot --check-leaks test/" 41 | }, 42 | "dependencies": { 43 | "node-uuid": "^1.4.2", 44 | "stormpath": "^0.9.0", 45 | "njwt": "0.0.1", 46 | "async": "^0.9.0", 47 | "underscore": "^1.8.2" 48 | }, 49 | "devDependencies": { 50 | "mocha": "^2.1.0", 51 | "supertest": "^0.15.0", 52 | "express": "^4.11.1", 53 | "body-parser": "^1.10.2", 54 | "istanbul": "^0.3.5", 55 | "njwt": "0.0.0", 56 | "cookie-parser": "^1.3.3", 57 | "pem": "^1.5.0" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "errors":{ 3 | "MISSING_COOKIE_MIDDLEWARE": "req.cookies does not exist - please configure cookie middleware", 4 | "MISSING_BODY_MIDDLEWARE": "req.body does not exist - please configure body middleware", 5 | "SP_CLIENT_UNINITIALIZED": "Stormpath client is not yet initialized", 6 | "authorization":{ 7 | "FORBIDDEN": "You are not permitted to access this resource", 8 | "FORBIDDEN_ORIGIN": "Forbidden Origin" 9 | }, 10 | "authentication":{ 11 | "UNAUTHENTICATED": "Authentication required", 12 | "BAD_PASSWORD_BODY": "Missing username or password in post body", 13 | "BAD_ACCESS_TOKEN_BODY": "Missing providerId or accessToken in post body", 14 | "UNKNOWN": "An error has occurred during authentication", 15 | "UNSUPPORTED_GRANT_TYPE": "Unsupported grant type" 16 | }, 17 | "xsrf":{ 18 | "XSRF_MISMATCH": "Invalid XSRF token" 19 | }, 20 | "library":{ 21 | "MISSING_API_KEY_ID": "API Key ID not found, please check your spConfig or environment variables", 22 | "MISSING_API_KEY_SECRET": "API Key Secret not found, please check your spConfig or environment variables", 23 | "MISSING_APP_HREF": "App Href not found, please check your spConfig or environment variables", 24 | "INVALID_SP_CONFIG": "Invalid spConfig", 25 | "SP_APP_UNINITIALIZED": "Stormpath application is not yet initialized", 26 | "SP_TENANT_UNINITIALIZED": "Stormpath tenant is not yet initialized", 27 | "NODE_SDK_ERROR": "Unknown error from Node SDK" 28 | } 29 | }, 30 | "configuration":{ 31 | "DEFAULT_TOKEN_ENDPOINT": "/oauth/token", 32 | "DEFAULT_LOGOUT_ENDPOINT": "/logout", 33 | "DEFAULT_ACCESS_TOKEN_COOKIE_NAME": "access_token", 34 | "DEFAULT_USER_COLLECTION_ENDPOINT": "/api/users", 35 | "DEFAULT_CURRENT_USER_ENDPOINT": "/api/users/current", 36 | "RESEND_EMAIL_VERIFICATION_ENDPOINT" : "/api/verificationEmails", 37 | "EMAIL_VERIFICATION_TOKEN_COLLECTION_ENDPOINT": "/api/emailVerificationTokens", 38 | "PASSWORD_RESET_TOKEN_COLLECTION_ENDPOINT": "/api/passwordResetTokens" 39 | } 40 | } -------------------------------------------------------------------------------- /test/authenticate.js: -------------------------------------------------------------------------------- 1 | var bodyParser = require('body-parser'); 2 | var cookieParser = require('cookie-parser'); 3 | var express = require('express'); 4 | var request = require('supertest'); 5 | var uuid = require('node-uuid'); 6 | 7 | var loginSuccessFixture = require('./fixtures/loginSuccess'); 8 | var mockLoginPost = {username:'abc',password:'123'}; 9 | var properties = require('../properties.json'); 10 | 11 | describe('authenticate middleware',function() { 12 | describe('with xsrf protection enabled',function(){ 13 | 14 | var app; 15 | var protectedEndpoint = '/protected-input'; 16 | var data = { hello: uuid() }; 17 | var agent; 18 | var xsrfToken; 19 | 20 | beforeEach(function(done){ 21 | loginSuccessFixture(function(fixture){ 22 | app = express(); 23 | app.use(bodyParser.json()); 24 | app.use(cookieParser()); 25 | var spMiddleware = require('../').createMiddleware({ 26 | appHref: fixture.appHref 27 | }); 28 | spMiddleware.attachDefaults(app); 29 | app.post(protectedEndpoint,spMiddleware.authenticate,function(req,res){ 30 | res.json(req.body); 31 | }); 32 | setTimeout(function(){ 33 | /* 34 | timeout is for allowing time for the api fixture to return the request 35 | for the applicatin - otherwise you get an application-not-ready error 36 | */ 37 | agent = request.agent(app); 38 | agent.post(properties.configuration.DEFAULT_TOKEN_ENDPOINT + '?grant_type=password') 39 | .send(mockLoginPost) 40 | .end(function(err,res){ 41 | xsrfToken = res.headers['set-cookie'][0].match(/XSRF-TOKEN=([^;]+)/)[1]; 42 | done(); 43 | }); 44 | },100); 45 | }); 46 | }); 47 | 48 | 49 | it('should reject post requests if the token is missing',function(done){ 50 | agent 51 | .post(protectedEndpoint) 52 | .send(data) 53 | .expect(401,{errorMessage:properties.errors.xsrf.XSRF_MISMATCH},done); 54 | }); 55 | 56 | it('should reject post requests if the token does not match',function(done){ 57 | agent 58 | .post(protectedEndpoint) 59 | .set('X-XSRF-TOKEN', 'not the token you gave me') 60 | .send(data) 61 | .expect(401,{errorMessage:properties.errors.xsrf.XSRF_MISMATCH},done); 62 | }); 63 | 64 | it('should accept post requests if the token is valid',function(done){ 65 | agent 66 | .post(protectedEndpoint) 67 | .set('X-XSRF-TOKEN', xsrfToken) 68 | .send(data) 69 | .expect(200,data,done); 70 | }); 71 | }); 72 | 73 | describe('with xsrf protection disabled',function(){ 74 | 75 | var app; 76 | var protectedEndpoint = '/protected-input'; 77 | var data = { hello: uuid() }; 78 | var agent; 79 | 80 | 81 | beforeEach(function(done){ 82 | loginSuccessFixture(function(fixture){ 83 | app = express(); 84 | app.use(bodyParser.json()); 85 | app.use(cookieParser()); 86 | var spMiddleware = require('../').createMiddleware({ 87 | appHref: fixture.appHref, 88 | xsrf: false 89 | }); 90 | spMiddleware.attachDefaults(app); 91 | app.post(protectedEndpoint,spMiddleware.authenticate,function(req,res){ 92 | res.json(req.body); 93 | }); 94 | setTimeout(function(){ 95 | /* 96 | timeout is for allowing time for the api fixture to return the request 97 | for the applicatin - otherwise you get an application-not-ready error 98 | */ 99 | agent = request.agent(app); 100 | agent.post(properties.configuration.DEFAULT_TOKEN_ENDPOINT + '?grant_type=password') 101 | .send(mockLoginPost) 102 | .end(done); 103 | },100); 104 | }); 105 | }); 106 | 107 | 108 | it('should accept post requests even though the xsrf token is not there',function(done){ 109 | agent 110 | .post(protectedEndpoint) 111 | .send(data) 112 | .expect(200,data,done); 113 | }); 114 | }); 115 | }); -------------------------------------------------------------------------------- /test/authenticateBearerAuthorizationHeader.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var bodyParser = require('body-parser'); 3 | var express = require('express'); 4 | var jwtErrors = require('njwt/properties.json').errors; 5 | var nJwt = require('njwt'); 6 | var request = require('supertest'); 7 | 8 | var itFixtureLoader = require('./it-fixtures/loader'); 9 | var properties = require('../properties.json'); 10 | var stormpathSdkExpress = require('../'); 11 | 12 | describe('authenticateBearerAuthorizationHeader',function() { 13 | 14 | var app; 15 | 16 | var fixture = itFixtureLoader('apiAuth.json'); 17 | 18 | 19 | var accessToken; 20 | 21 | var protectedEndpoint = '/input'; 22 | var postData = { hello: 'world' }; 23 | 24 | 25 | before(function(done){ 26 | var spMiddleware = stormpathSdkExpress.createMiddleware({ 27 | appHref: fixture.appHref, 28 | apiKeyId: fixture.apiKeyId, 29 | apiKeySecret: fixture.apiKeySecret 30 | }); 31 | app = express(); 32 | app.use(bodyParser.json()); 33 | spMiddleware.attachDefaults(app); 34 | app.use(spMiddleware.authenticate); 35 | app.post(protectedEndpoint,function(req,res){ 36 | res.json({ data: req.body, user: req.user }); 37 | }); 38 | 39 | var wait = setInterval(function(){ 40 | /* wait for sp application */ 41 | if(spMiddleware.getApplication()){ 42 | clearInterval(wait); 43 | request(app) 44 | .post(properties.configuration.DEFAULT_TOKEN_ENDPOINT + '?grant_type=client_credentials') 45 | .set('Authorization', 'Basic ' + 46 | new Buffer(fixture.accountApiKeyId+':'+fixture.accountApiKeySecret) 47 | .toString('base64') 48 | ) 49 | .end(function(err,res){ 50 | accessToken = res.body.access_token; 51 | done(); 52 | }); 53 | } 54 | },100); 55 | 56 | }); 57 | 58 | describe('posting a malformed Authorization: Bearer value',function(){ 59 | it('should error',function(done){ 60 | request(app) 61 | .post(protectedEndpoint) 62 | .set('Authorization', 'Bearer blah') 63 | .expect(400,done); 64 | }); 65 | }); 66 | describe('posting a valid brearer token',function(){ 67 | it('should authorize the request',function(done){ 68 | request(app) 69 | .post(protectedEndpoint) 70 | .set('Authorization', 'Bearer ' + accessToken) 71 | .send(postData) 72 | .end(function(err,res){ 73 | assert.deepEqual(res.body.data,postData); 74 | assert.equal(res.body.user.href,fixture.accountHref); 75 | done(); 76 | }); 77 | }); 78 | }); 79 | describe('posting a spoofed brearer token',function(){ 80 | var fakeToken = nJwt.Jwt({ 81 | sub: 'me' 82 | }).signWith('HS256','my fake key').compact(); 83 | it('should error',function(done){ 84 | request(app) 85 | .post(protectedEndpoint) 86 | .set('Authorization', 'Bearer ' + fakeToken) 87 | .send(postData) 88 | .expect(401,{errorMessage:jwtErrors.SIGNATURE_MISMTACH},done); 89 | }); 90 | }); 91 | describe('posting an expired brearer token',function(){ 92 | var app, expiredToken; 93 | /* 94 | Creating another app that will issue tokens whicn expire 95 | in 1 second 96 | */ 97 | before(function(done){ 98 | var spMiddleware = require('../').createMiddleware({ 99 | appHref: fixture.appHref, 100 | apiKeyId: fixture.apiKeyId, 101 | apiKeySecret: fixture.apiKeySecret, 102 | accessTokenTtl: 0 103 | }); 104 | app = express(); 105 | app.use(bodyParser.json()); 106 | spMiddleware.attachDefaults(app); 107 | app.post(protectedEndpoint,spMiddleware.authenticate,function(req,res){ 108 | res.json({ data: req.body, user: req.user }); 109 | }); 110 | 111 | var wait = setInterval(function(){ 112 | /* wait for sp application */ 113 | if(spMiddleware.getApplication()){ 114 | clearInterval(wait); 115 | request(app) 116 | .post(properties.configuration.DEFAULT_TOKEN_ENDPOINT + '?grant_type=client_credentials') 117 | .set('Authorization', 'Basic ' + 118 | new Buffer(fixture.accountApiKeyId+':'+fixture.accountApiKeySecret) 119 | .toString('base64') 120 | ) 121 | .end(function(err,res){ 122 | expiredToken = res.body.access_token; 123 | assert.equal(res.body.expires_in,0); 124 | setTimeout(done,1000); 125 | }); 126 | } 127 | },100); 128 | }); 129 | it('should error',function(done){ 130 | request(app) 131 | .post(protectedEndpoint) 132 | .set('Authorization', 'Bearer ' + expiredToken) 133 | .send(postData) 134 | .expect(401,{errorMessage:jwtErrors.EXPIRED},done); 135 | }); 136 | }); 137 | }); -------------------------------------------------------------------------------- /test/authenticateForToken.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var bodyParser = require('body-parser'); 3 | var express = require('express'); 4 | var https = require('https'); 5 | var nJwt = require('njwt'); 6 | var pem = require('pem'); 7 | var request = require('supertest'); 8 | 9 | var itFixtureLoader = require('./it-fixtures/loader'); 10 | var properties = require('../properties'); 11 | 12 | describe('authenticateForToken',function() { 13 | 14 | var app, server; 15 | 16 | var apiAuthFixture = itFixtureLoader('apiAuth.json'); 17 | var loginAuthFixture = itFixtureLoader('loginAuth.json'); 18 | 19 | var jwtExpr = /[^\.]+\.[^\.]+\.[^;]+/; 20 | var httpsOnlyCookieExpr = /access_token=[^\.]+\.[^\.]+\.[^;]+; Expires=[^;]+; Secure; HttpOnly;/; 21 | var customScope = 'my-custom scope'; 22 | var customRequestedScope = 'quiero'; 23 | var parser = nJwt.Parser().setSigningKey(apiAuthFixture.apiKeySecret); 24 | 25 | before(function(done){ 26 | var spMiddleware = require('../').createMiddleware({ 27 | appHref: apiAuthFixture.appHref, 28 | apiKeyId: apiAuthFixture.apiKeyId, 29 | apiKeySecret: apiAuthFixture.apiKeySecret, 30 | scopeFactory: function(req,res,authenticationResult,account,requestedScope,done) { 31 | done(null,requestedScope ? requestedScopeReflection(customScope,customRequestedScope) : ''); 32 | } 33 | }); 34 | app = express(); 35 | app.use(bodyParser.json()); 36 | 37 | spMiddleware.attachDefaults(app); 38 | 39 | 40 | pem.createCertificate({days:1, selfSigned:true}, function(err, keys){ 41 | server = https.createServer({key: keys.serviceKey, cert: keys.certificate}, app).listen(0); 42 | }); 43 | 44 | var wait = setInterval(function(){ 45 | /* wait for sp application */ 46 | if(spMiddleware.getApplication()){ 47 | clearInterval(wait); 48 | done(); 49 | } 50 | },100); 51 | 52 | }); 53 | 54 | function requestedScopeReflection(customScope,requestedScope){ 55 | return [customScope,requestedScope].join(' '); 56 | } 57 | 58 | describe('if request has no grant_type',function(){ 59 | it('should error',function(done){ 60 | request(server) 61 | .post(properties.configuration.DEFAULT_TOKEN_ENDPOINT) 62 | .expect(401,{errorMessage:properties.errors.authentication.UNSUPPORTED_GRANT_TYPE},done); 63 | }); 64 | }); 65 | describe('if request has an unsupported grant_type',function(){ 66 | it('should error',function(done){ 67 | request(app) 68 | .post(properties.configuration.DEFAULT_TOKEN_ENDPOINT + '?grant_type=foo') 69 | .expect(401,{errorMessage:properties.errors.authentication.UNSUPPORTED_GRANT_TYPE},done); 70 | }); 71 | }); 72 | 73 | describe('if grant_type=client_credentials',function(){ 74 | var tokenEndpoint = properties.configuration.DEFAULT_TOKEN_ENDPOINT + '?grant_type=client_credentials'; 75 | describe('and request has a malformed Authorization value',function(){ 76 | it('should error',function(done){ 77 | request(app) 78 | .post(tokenEndpoint) 79 | .set('Authorization', 'blah') 80 | .expect(400,{errorMessage:'Invalid Authorization value'},done); 81 | }); 82 | }); 83 | describe('and request has a malformed Authorization: Basic value',function(){ 84 | it('should error',function(done){ 85 | request(app) 86 | .post(tokenEndpoint) 87 | .set('Authorization', 'Basic blah') 88 | .expect(400,{errorMessage:'Invalid Authorization value'},done); 89 | }); 90 | }); 91 | describe('and request has valid api key credentials',function(){ 92 | it('should respond with a token',function(done){ 93 | request(app) 94 | .post(tokenEndpoint) 95 | .set('Authorization', 'Basic ' + 96 | new Buffer(apiAuthFixture.accountApiKeyId+':'+apiAuthFixture.accountApiKeySecret) 97 | .toString('base64') 98 | ) 99 | .end(function(err,res){ 100 | assert.equal(res.status,200); 101 | assert.equal(typeof res.body, 'object'); 102 | assert.equal(res.body.token_type,'Bearer'); 103 | assert.equal(res.body.expires_in,3600); 104 | assert(jwtExpr.test(res.body.access_token)); 105 | parser.parseClaimsJws(res.body.access_token,function(err,jwt) { 106 | assert.equal(jwt.body.jti.length,36,'is a uuid'); 107 | assert.equal(jwt.body.iss,apiAuthFixture.appHref,'Was issued by the application'); 108 | assert.equal(jwt.body.sub,apiAuthFixture.accountApiKeyId,'subject is the api key id'); 109 | assert.equal(jwt.body.scope,undefined,'no scopes by default'); 110 | done(); 111 | }); 112 | }); 113 | }); 114 | it('should preserve scope from the scope factory in the access token',function(done){ 115 | request(app) 116 | .post(tokenEndpoint+'&scope='+customRequestedScope) 117 | .set('Authorization', 'Basic ' + 118 | new Buffer(apiAuthFixture.accountApiKeyId+':'+apiAuthFixture.accountApiKeySecret) 119 | .toString('base64') 120 | ) 121 | .end(function(err,res) { 122 | assert.equal(res.body.scope,requestedScopeReflection(customScope,customRequestedScope)); 123 | parser.parseClaimsJws(res.body.access_token,function(err,jwt) { 124 | assert.equal(jwt.body.scope,requestedScopeReflection(customScope,customRequestedScope)); 125 | done(); 126 | }); 127 | }); 128 | }); 129 | }); 130 | }); 131 | describe('if grant_type=password',function(){ 132 | describe('and request has valid username and password',function(){ 133 | it('should respond with a token',function(done){ 134 | request(server) 135 | .post(properties.configuration.DEFAULT_TOKEN_ENDPOINT + '?grant_type=password') 136 | .send({ 137 | username:loginAuthFixture.accountUsername, 138 | password:loginAuthFixture.accountPassword 139 | }) 140 | .expect('set-cookie', httpsOnlyCookieExpr) 141 | .expect(201,'') 142 | .end(function(err,res) { 143 | if(err){ 144 | throw err; 145 | } 146 | var access_token = res.headers['set-cookie'][1].match(/access_token=([^;]+)/)[1]; 147 | parser.parseClaimsJws(access_token,function(err,jwt) { 148 | assert.equal(err,null); 149 | assert.equal(jwt.body.jti.length,36,'is a uuid'); 150 | assert.equal(jwt.body.iss,apiAuthFixture.appHref,'Was issued by the application'); 151 | assert.equal(jwt.body.sub,loginAuthFixture.accountHref,'subject is the account'); 152 | assert.equal(jwt.body.scope,undefined,'no scopes by default'); 153 | done(); 154 | }); 155 | }); 156 | 157 | }); 158 | }); 159 | describe('and request has invalid password',function(){ 160 | it('should error',function(done){ 161 | request(app) 162 | .post(properties.configuration.DEFAULT_TOKEN_ENDPOINT + '?grant_type=password') 163 | .send({ 164 | username:loginAuthFixture.accountUsername, 165 | password: 'not the right password' 166 | }) 167 | .expect(401,{ 168 | code: 7100, 169 | errorMessage:'Invalid username or password.' 170 | },done); 171 | }); 172 | }); 173 | }); 174 | }); -------------------------------------------------------------------------------- /test/authenticateSocialForToken.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var bodyParser = require('body-parser'); 3 | var express = require('express'); 4 | var https = require('https'); 5 | var nJwt = require('njwt'); 6 | var pem = require('pem'); 7 | var request = require('supertest'); 8 | 9 | var loginSuccessFixture = require('./fixtures/loginSuccess'); 10 | var properties = require('../properties.json'); 11 | var stormpathSdkExpress = require('../'); 12 | 13 | describe('authenticateSocialForToken',function() { 14 | 15 | var tokenEndpoint = properties.configuration.DEFAULT_TOKEN_ENDPOINT + '?grant_type=social'; 16 | 17 | describe('if the sp app is not yet initialized',function(){ 18 | var app; 19 | 20 | /* 21 | Mock context. Manually proviede an undfined value 22 | for the application 23 | */ 24 | 25 | var spConfig = { 26 | spClient: { 27 | getApplication: function(href,cb){ 28 | cb(null,undefined); 29 | }, 30 | getCurrentTenant: function(cb){ 31 | cb(null,undefined); 32 | } 33 | } 34 | }; 35 | 36 | before(function(){ 37 | app = express(); 38 | app.use(stormpathSdkExpress.AuthenticateSocialForToken(spConfig)); 39 | }); 40 | it('should error',function(done){ 41 | request(app) 42 | .post('/') 43 | .expect(500,{errorMessage:properties.errors.library.SP_APP_UNINITIALIZED},done); 44 | }); 45 | }); 46 | 47 | 48 | describe('if the request does not contain providerId & accessToken',function(){ 49 | 50 | var app; 51 | 52 | // Manually provide a mock application so that we don't fail in that clause 53 | 54 | var spConfig = { 55 | spClient: { 56 | getApplication: function(href,cb){ 57 | cb(null,{}); 58 | }, 59 | getCurrentTenant: function(cb){ 60 | cb(null,undefined); 61 | } 62 | } 63 | }; 64 | 65 | before(function(){ 66 | app = express(); 67 | app.use(stormpathSdkExpress.AuthenticateSocialForToken(spConfig)); 68 | }); 69 | 70 | it('should error',function(done){ 71 | 72 | request(app) 73 | .post('/') 74 | .expect(400,{errorMessage:properties.errors.authentication.BAD_ACCESS_TOKEN_BODY},done); 75 | 76 | }); 77 | 78 | }); 79 | 80 | 81 | describe('with a request that contains a valid accessToken-based login',function(){ 82 | var jwtExpr = /[^\.]+\.[^\.]+\.[^;]+/; 83 | var httpOnlyCookieExpr = /access_token=[^\.]+\.[^\.]+\.[^;]+; Expires=[^;]+; HttpOnly;/; 84 | var httpsOnlyCookieExpr = /access_token=[^\.]+\.[^\.]+\.[^;]+; Expires=[^;]+; Secure; HttpOnly;/; 85 | var xsrfTokenCookieExpr = /XSRF-TOKEN=[0-9A-Za-z\-]+; Expires=[^;]+;/; 86 | 87 | var mockLoginPost = {providerId:'facebook',accessToken:'123'}; 88 | var parser = nJwt.Parser().setSigningKey('123'); 89 | var customRequestedScope = 'quiero'; 90 | var customScope = 'my-custom scope'; 91 | 92 | 93 | describe('and default spConfig options with an https server',function(){ 94 | 95 | var app, server; 96 | 97 | function requestedScopeReflection(customScope,requestedScope){ 98 | return [customScope,requestedScope].join(' '); 99 | } 100 | 101 | before(function(done){ 102 | loginSuccessFixture(function(fixture){ 103 | var spMiddleware = stormpathSdkExpress.createMiddleware({ 104 | appHref: fixture.appHref, 105 | apiKeyId: '123', 106 | apiKeySecret: '123', 107 | scopeFactory: function(req,res,authenticationResult,account,requestedScope,done) { 108 | done(null,requestedScope ? requestedScopeReflection(customScope,customRequestedScope) : ''); 109 | } 110 | }); 111 | app = express(); 112 | app.use(bodyParser.json()); 113 | spMiddleware.attachDefaults(app); 114 | 115 | pem.createCertificate({days:1, selfSigned:true}, function(err, keys){ 116 | server = https.createServer({key: keys.serviceKey, cert: keys.certificate}, app).listen(0); 117 | var wait = setInterval(function(){ 118 | /* wait for sp application */ 119 | if(spMiddleware.getApplication()){ 120 | clearInterval(wait); 121 | done(); 122 | } 123 | },100); 124 | }); 125 | 126 | 127 | }); 128 | }); 129 | 130 | 131 | 132 | it('should write an access token in a Secure, HttpOnly cookie',function(done){ 133 | request(server) 134 | .post(tokenEndpoint) 135 | .send(mockLoginPost) 136 | .expect('set-cookie', httpsOnlyCookieExpr) 137 | .expect(201,'',done); 138 | }); 139 | 140 | describe('and scope is requested',function(){ 141 | describe('via URL params',function(){ 142 | it('should preserve scope from the scope factory in the access token',function(done){ 143 | request(server) 144 | .post(tokenEndpoint+'&scope='+customRequestedScope) 145 | .send(mockLoginPost) 146 | .expect('set-cookie', httpsOnlyCookieExpr) 147 | .expect(201,'') 148 | .end(function(err,res) { 149 | var access_token = res.headers['set-cookie'][1].match(/access_token=([^;]+)/)[1]; 150 | parser.parseClaimsJws(access_token,function(err,jwt) { 151 | assert.equal(jwt.body.scope,requestedScopeReflection(customScope,customRequestedScope)); 152 | done(); 153 | }); 154 | }); 155 | }); 156 | }); 157 | 158 | describe('via post body',function(){ 159 | it('should preserve scope from the scope factory in the access token',function(done){ 160 | request(server) 161 | .post(tokenEndpoint) 162 | .send({providerId:mockLoginPost.providerId,accessToken:mockLoginPost.accessToken,scope:customRequestedScope}) 163 | .expect('set-cookie', httpsOnlyCookieExpr) 164 | .expect(201,'') 165 | .end(function(err,res) { 166 | var access_token = res.headers['set-cookie'][1].match(/access_token=([^;]+)/)[1]; 167 | parser.parseClaimsJws(access_token,function(err,jwt) { 168 | assert.equal(jwt.body.scope,requestedScopeReflection(customScope,customRequestedScope)); 169 | done(); 170 | }); 171 | }); 172 | }); 173 | }); 174 | }); 175 | it('should write an xsrf cookie',function(done){ 176 | request(app) 177 | .post(tokenEndpoint) 178 | .send(mockLoginPost) 179 | .expect('set-cookie', xsrfTokenCookieExpr) 180 | .expect(201,'',done); 181 | }); 182 | 183 | it('should not write a response body',function(done){ 184 | request(app) 185 | .post(tokenEndpoint) 186 | .send(mockLoginPost) 187 | .expect(201,'',done); 188 | }); 189 | }); 190 | 191 | describe('and default spConfig options with an http server',function(){ 192 | 193 | var app; 194 | 195 | before(function(done){ 196 | loginSuccessFixture(function(fixture){ 197 | var spMiddleware = stormpathSdkExpress.createMiddleware({ 198 | appHref: fixture.appHref 199 | }); 200 | app = express(); 201 | app.use(bodyParser.json()); 202 | spMiddleware.attachDefaults(app); 203 | var wait = setInterval(function(){ 204 | /* wait for sp application */ 205 | if(spMiddleware.getApplication()){ 206 | clearInterval(wait); 207 | done(); 208 | } 209 | },100); 210 | }); 211 | }); 212 | 213 | it('should write an access token to an http-only cookie',function(done){ 214 | 215 | request(app) 216 | .post(tokenEndpoint) 217 | .send(mockLoginPost) 218 | .expect('set-cookie', httpOnlyCookieExpr, done); 219 | 220 | }); 221 | it('should write an empty body with 201 response',function(done){ 222 | 223 | request(app) 224 | .post(tokenEndpoint) 225 | .send(mockLoginPost) 226 | .expect(201,'',done); 227 | 228 | }); 229 | }); 230 | 231 | describe('and spConfig { forceHttps: true } option with an http server',function(){ 232 | 233 | var app; 234 | 235 | before(function(done){ 236 | loginSuccessFixture(function(fixture){ 237 | var spMiddleware = stormpathSdkExpress.createMiddleware({ 238 | appHref: fixture.appHref, 239 | forceHttps: true 240 | }); 241 | app = express(); 242 | app.use(bodyParser.json()); 243 | spMiddleware.attachDefaults(app); 244 | 245 | var wait = setInterval(function(){ 246 | /* wait for sp application */ 247 | if(spMiddleware.getApplication()){ 248 | clearInterval(wait); 249 | done(); 250 | } 251 | },100); 252 | }); 253 | }); 254 | 255 | it('should write an access token in a Secure, HttpOnly cookie',function(done){ 256 | request(app) 257 | .post(tokenEndpoint) 258 | .send(mockLoginPost) 259 | .expect('set-cookie', httpsOnlyCookieExpr) 260 | .expect(201,'',done); 261 | }); 262 | 263 | it('should write an empty body with 201 response',function(done){ 264 | 265 | request(app) 266 | .post(tokenEndpoint) 267 | .send(mockLoginPost) 268 | .expect(201,'',done); 269 | 270 | }); 271 | }); 272 | 273 | describe('and {writeAccessTokenResponse: true} spConfig option',function(){ 274 | 275 | var app, server; 276 | 277 | before(function(done){ 278 | loginSuccessFixture(function(fixture){ 279 | app = express(); 280 | var spMiddleware = stormpathSdkExpress.createMiddleware({ 281 | appHref: fixture.appHref, 282 | apiKeyId: '123', 283 | apiKeySecret: '123', 284 | writeAccessTokenResponse: true, 285 | scopeFactory: function(req,res,authenticationResult,account,requstedScope,done) { 286 | done(null,customScope); 287 | } 288 | }); 289 | app.use(bodyParser.json()); 290 | spMiddleware.attachDefaults(app); 291 | 292 | pem.createCertificate({days:1, selfSigned:true}, function(err, keys){ 293 | server = https.createServer({key: keys.serviceKey, cert: keys.certificate}, app).listen(0); 294 | var wait = setInterval(function(){ 295 | /* wait for sp application */ 296 | if(spMiddleware.getApplication()){ 297 | clearInterval(wait); 298 | done(); 299 | } 300 | },100); 301 | }); 302 | 303 | }); 304 | }); 305 | 306 | it('should write an access token in a Secure, HttpOnly cookie',function(done){ 307 | request(server) 308 | .post(tokenEndpoint) 309 | .send(mockLoginPost) 310 | .expect('set-cookie', httpsOnlyCookieExpr, done); 311 | }); 312 | 313 | it('should write access tokens to the response bodies',function(done){ 314 | 315 | request(server) 316 | .post(tokenEndpoint) 317 | .send(mockLoginPost) 318 | .end(function(err,res){ 319 | assert(res.body.access_token.match(jwtExpr)); 320 | assert(res.body.token_type==='Bearer'); 321 | assert(res.body.expires_in===3600); 322 | done(); 323 | }); 324 | 325 | }); 326 | 327 | it('should preserve scope from the scope factory in the response body and access token',function(done){ 328 | request(server) 329 | .post(tokenEndpoint+'&scope='+customRequestedScope) 330 | .send(mockLoginPost) 331 | .end(function(err,res) { 332 | var access_token = res.headers['set-cookie'][1].match(/access_token=([^;]+)/)[1]; 333 | 334 | assert.equal(res.body.scope,customScope); 335 | parser.parseClaimsJws(access_token,function(err,jwt) { 336 | assert.equal(jwt.body.scope,customScope); 337 | done(); 338 | }); 339 | }); 340 | }); 341 | }); 342 | 343 | describe('and {writeAccessTokenResponse: true, writeAccessTokenToCookie: false} spConfig options',function(){ 344 | 345 | var app; 346 | 347 | before(function(done){ 348 | loginSuccessFixture(function(fixture){ 349 | var spMiddleware = stormpathSdkExpress.createMiddleware({ 350 | appHref: fixture.appHref, 351 | writeAccessTokenResponse: true, 352 | writeAccessTokenToCookie: false 353 | }); 354 | app = express(); 355 | app.use(bodyParser.json()); 356 | spMiddleware.attachDefaults(app); 357 | var wait = setInterval(function(){ 358 | /* wait for sp application */ 359 | if(spMiddleware.getApplication()){ 360 | clearInterval(wait); 361 | done(); 362 | } 363 | },100); 364 | }); 365 | }); 366 | 367 | it('should not write an access token cookie',function(done){ 368 | request(app) 369 | .post(tokenEndpoint) 370 | .send(mockLoginPost) 371 | .end(function(err,res){ 372 | assert(httpsOnlyCookieExpr.test(res.headers['set-cookie'].join(','))===false); 373 | done(); 374 | }); 375 | }); 376 | 377 | it('should write an access token tresponse body',function(done){ 378 | 379 | request(app) 380 | .post(tokenEndpoint) 381 | .send(mockLoginPost) 382 | .end(function(err,res){ 383 | assert(res.body.access_token.match(jwtExpr)); 384 | assert(res.body.token_type==='Bearer'); 385 | assert(res.body.expires_in===3600); 386 | done(); 387 | }); 388 | 389 | }); 390 | }); 391 | 392 | describe('and { writeAccessTokenToCookie: false} spConfig options',function(){ 393 | 394 | var app; 395 | 396 | before(function(done){ 397 | loginSuccessFixture(function(fixture){ 398 | var spMiddleware = stormpathSdkExpress.createMiddleware({ 399 | appHref: fixture.appHref, 400 | writeAccessTokenToCookie: false 401 | }); 402 | app = express(); 403 | app.use(bodyParser.json()); 404 | spMiddleware.attachDefaults(app); 405 | var wait = setInterval(function(){ 406 | /* wait for sp application */ 407 | if(spMiddleware.getApplication()){ 408 | clearInterval(wait); 409 | done(); 410 | } 411 | },100); 412 | }); 413 | }); 414 | 415 | it('should not write an access token cookie',function(done){ 416 | request(app) 417 | .post(tokenEndpoint) 418 | .send(mockLoginPost) 419 | .end(function(err,res){ 420 | assert(httpOnlyCookieExpr.test(res.headers['set-cookie'].join(','))===false); 421 | assert(httpsOnlyCookieExpr.test(res.headers['set-cookie'].join(','))===false); 422 | done(); 423 | }); 424 | }); 425 | 426 | it('should write an empty body with 201 response',function(done){ 427 | request(app) 428 | .post(tokenEndpoint) 429 | .send(mockLoginPost) 430 | .expect(201,'',done); 431 | }); 432 | }); 433 | 434 | 435 | }); 436 | 437 | 438 | 439 | }); -------------------------------------------------------------------------------- /test/authenticateUsernamePasswordForToken.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var bodyParser = require('body-parser'); 3 | var express = require('express'); 4 | var https = require('https'); 5 | var nJwt = require('njwt'); 6 | var pem = require('pem'); 7 | var request = require('supertest'); 8 | 9 | var loginSuccessFixture = require('./fixtures/loginSuccess'); 10 | var properties = require('../properties.json'); 11 | var stormpathSdkExpress = require('../'); 12 | 13 | describe('authenticateUsernamePasswordForToken',function() { 14 | 15 | var tokenEndpoint = properties.configuration.DEFAULT_TOKEN_ENDPOINT + '?grant_type=password'; 16 | 17 | describe('if the sp app is not yet initialized',function(){ 18 | var app; 19 | 20 | /* 21 | Mock context. Manually proviede an undfined value 22 | for the application 23 | */ 24 | 25 | var spConfig = { 26 | spClient: { 27 | getApplication: function(href,cb){ 28 | cb(null,undefined); 29 | }, 30 | getCurrentTenant: function(cb){ 31 | cb(null,undefined); 32 | } 33 | } 34 | }; 35 | 36 | before(function(){ 37 | app = express(); 38 | app.use(stormpathSdkExpress.AuthenticateUsernamePasswordForToken(spConfig)); 39 | }); 40 | it('should error',function(done){ 41 | request(app) 42 | .post('/') 43 | .expect(500,{errorMessage:properties.errors.library.SP_APP_UNINITIALIZED},done); 44 | }); 45 | }); 46 | 47 | 48 | describe('if the request does not contain login & password',function(){ 49 | 50 | var app; 51 | 52 | // Manually provide a mock application so that we don't fail in that clause 53 | 54 | var spConfig = { 55 | spClient: { 56 | getApplication: function(href,cb){ 57 | cb(null,{}); 58 | }, 59 | getCurrentTenant: function(cb){ 60 | cb(null,undefined); 61 | } 62 | } 63 | }; 64 | 65 | before(function(){ 66 | app = express(); 67 | app.use(stormpathSdkExpress.AuthenticateUsernamePasswordForToken(spConfig)); 68 | }); 69 | 70 | it('should error',function(done){ 71 | 72 | request(app) 73 | .post('/') 74 | .expect(400,{errorMessage:properties.errors.authentication.BAD_PASSWORD_BODY},done); 75 | 76 | }); 77 | 78 | }); 79 | 80 | 81 | describe('with a request that contains a valid password-based login',function(){ 82 | var jwtExpr = /[^\.]+\.[^\.]+\.[^;]+/; 83 | var httpOnlyCookieExpr = /access_token=[^\.]+\.[^\.]+\.[^;]+; Expires=[^;]+; HttpOnly;/; 84 | var httpsOnlyCookieExpr = /access_token=[^\.]+\.[^\.]+\.[^;]+; Expires=[^;]+; Secure; HttpOnly;/; 85 | var xsrfTokenCookieExpr = /XSRF-TOKEN=[0-9A-Za-z\-]+; Expires=[^;]+;/; 86 | 87 | var mockLoginPost = {username:'abc',password:'123'}; 88 | var parser = nJwt.Parser().setSigningKey('123'); 89 | var customRequestedScope = 'quiero'; 90 | var customScope = 'my-custom scope'; 91 | 92 | 93 | describe('and default spConfig options with an https server',function(){ 94 | 95 | var app, server; 96 | 97 | function requestedScopeReflection(customScope,requestedScope){ 98 | return [customScope,requestedScope].join(' '); 99 | } 100 | 101 | before(function(done){ 102 | loginSuccessFixture(function(fixture){ 103 | var spMiddleware = stormpathSdkExpress.createMiddleware({ 104 | appHref: fixture.appHref, 105 | apiKeyId: '123', 106 | apiKeySecret: '123', 107 | scopeFactory: function(req,res,authenticationResult,account,requestedScope,done) { 108 | done(null,requestedScope ? requestedScopeReflection(customScope,customRequestedScope) : ''); 109 | } 110 | }); 111 | app = express(); 112 | app.use(bodyParser.json()); 113 | spMiddleware.attachDefaults(app); 114 | 115 | pem.createCertificate({days:1, selfSigned:true}, function(err, keys){ 116 | server = https.createServer({key: keys.serviceKey, cert: keys.certificate}, app).listen(0); 117 | var wait = setInterval(function(){ 118 | /* wait for sp application */ 119 | if(spMiddleware.getApplication()){ 120 | clearInterval(wait); 121 | done(); 122 | } 123 | },100); 124 | }); 125 | 126 | 127 | }); 128 | }); 129 | 130 | 131 | 132 | it('should write an access token in a Secure, HttpOnly cookie',function(done){ 133 | request(server) 134 | .post(tokenEndpoint) 135 | .send(mockLoginPost) 136 | .expect('set-cookie', httpsOnlyCookieExpr) 137 | .expect(201,'',done); 138 | }); 139 | 140 | describe('and scope is requested',function(){ 141 | describe('via URL params',function(){ 142 | it('should preserve scope from the scope factory in the access token',function(done){ 143 | request(server) 144 | .post(tokenEndpoint+'&scope='+customRequestedScope) 145 | .send(mockLoginPost) 146 | .expect('set-cookie', httpsOnlyCookieExpr) 147 | .expect(201,'') 148 | .end(function(err,res) { 149 | var access_token = res.headers['set-cookie'][1].match(/access_token=([^;]+)/)[1]; 150 | parser.parseClaimsJws(access_token,function(err,jwt) { 151 | assert.equal(jwt.body.scope,requestedScopeReflection(customScope,customRequestedScope)); 152 | done(); 153 | }); 154 | }); 155 | }); 156 | }); 157 | 158 | describe('via post body',function(){ 159 | it('should preserve scope from the scope factory in the access token',function(done){ 160 | request(server) 161 | .post(tokenEndpoint) 162 | .send({username:mockLoginPost.username,password:mockLoginPost.password,scope:customRequestedScope}) 163 | .expect('set-cookie', httpsOnlyCookieExpr) 164 | .expect(201,'') 165 | .end(function(err,res) { 166 | var access_token = res.headers['set-cookie'][1].match(/access_token=([^;]+)/)[1]; 167 | parser.parseClaimsJws(access_token,function(err,jwt) { 168 | assert.equal(jwt.body.scope,requestedScopeReflection(customScope,customRequestedScope)); 169 | done(); 170 | }); 171 | }); 172 | }); 173 | }); 174 | }); 175 | it('should write an xsrf cookie',function(done){ 176 | request(app) 177 | .post(tokenEndpoint) 178 | .send(mockLoginPost) 179 | .expect('set-cookie', xsrfTokenCookieExpr) 180 | .expect(201,'',done); 181 | }); 182 | 183 | it('should not write a response body',function(done){ 184 | request(app) 185 | .post(tokenEndpoint) 186 | .send(mockLoginPost) 187 | .expect(201,'',done); 188 | }); 189 | }); 190 | 191 | describe('and default spConfig options with an http server',function(){ 192 | 193 | var app; 194 | 195 | before(function(done){ 196 | loginSuccessFixture(function(fixture){ 197 | var spMiddleware = stormpathSdkExpress.createMiddleware({ 198 | appHref: fixture.appHref 199 | }); 200 | app = express(); 201 | app.use(bodyParser.json()); 202 | spMiddleware.attachDefaults(app); 203 | var wait = setInterval(function(){ 204 | /* wait for sp application */ 205 | if(spMiddleware.getApplication()){ 206 | clearInterval(wait); 207 | done(); 208 | } 209 | },100); 210 | }); 211 | }); 212 | 213 | it('should write an access token to an http-only cookie',function(done){ 214 | 215 | request(app) 216 | .post(tokenEndpoint) 217 | .send(mockLoginPost) 218 | .expect('set-cookie', httpOnlyCookieExpr, done); 219 | 220 | }); 221 | it('should write an empty body with 201 response',function(done){ 222 | 223 | request(app) 224 | .post(tokenEndpoint) 225 | .send(mockLoginPost) 226 | .expect(201,'',done); 227 | 228 | }); 229 | }); 230 | 231 | describe('and spConfig { forceHttps: true } option with an http server',function(){ 232 | 233 | var app; 234 | 235 | before(function(done){ 236 | loginSuccessFixture(function(fixture){ 237 | var spMiddleware = stormpathSdkExpress.createMiddleware({ 238 | appHref: fixture.appHref, 239 | forceHttps: true 240 | }); 241 | app = express(); 242 | app.use(bodyParser.json()); 243 | spMiddleware.attachDefaults(app); 244 | 245 | var wait = setInterval(function(){ 246 | /* wait for sp application */ 247 | if(spMiddleware.getApplication()){ 248 | clearInterval(wait); 249 | done(); 250 | } 251 | },100); 252 | }); 253 | }); 254 | 255 | it('should write an access token in a Secure, HttpOnly cookie',function(done){ 256 | request(app) 257 | .post(tokenEndpoint) 258 | .send(mockLoginPost) 259 | .expect('set-cookie', httpsOnlyCookieExpr) 260 | .expect(201,'',done); 261 | }); 262 | 263 | it('should write an empty body with 201 response',function(done){ 264 | 265 | request(app) 266 | .post(tokenEndpoint) 267 | .send(mockLoginPost) 268 | .expect(201,'',done); 269 | 270 | }); 271 | }); 272 | 273 | describe('and {writeAccessTokenResponse: true} spConfig option',function(){ 274 | 275 | var app, server; 276 | 277 | before(function(done){ 278 | loginSuccessFixture(function(fixture){ 279 | app = express(); 280 | var spMiddleware = stormpathSdkExpress.createMiddleware({ 281 | appHref: fixture.appHref, 282 | apiKeyId: '123', 283 | apiKeySecret: '123', 284 | writeAccessTokenResponse: true, 285 | scopeFactory: function(req,res,authenticationResult,account,requstedScope,done) { 286 | done(null,customScope); 287 | } 288 | }); 289 | app.use(bodyParser.json()); 290 | spMiddleware.attachDefaults(app); 291 | 292 | pem.createCertificate({days:1, selfSigned:true}, function(err, keys){ 293 | server = https.createServer({key: keys.serviceKey, cert: keys.certificate}, app).listen(0); 294 | var wait = setInterval(function(){ 295 | /* wait for sp application */ 296 | if(spMiddleware.getApplication()){ 297 | clearInterval(wait); 298 | done(); 299 | } 300 | },100); 301 | }); 302 | 303 | }); 304 | }); 305 | 306 | it('should write an access token in a Secure, HttpOnly cookie',function(done){ 307 | request(server) 308 | .post(tokenEndpoint) 309 | .send(mockLoginPost) 310 | .expect('set-cookie', httpsOnlyCookieExpr, done); 311 | }); 312 | 313 | it('should write access tokens to the response bodies',function(done){ 314 | 315 | request(server) 316 | .post(tokenEndpoint) 317 | .send(mockLoginPost) 318 | .end(function(err,res){ 319 | assert(res.body.access_token.match(jwtExpr)); 320 | assert(res.body.token_type==='Bearer'); 321 | assert(res.body.expires_in===3600); 322 | done(); 323 | }); 324 | 325 | }); 326 | 327 | it('should preserve scope from the scope factory in the response body and access token',function(done){ 328 | request(server) 329 | .post(tokenEndpoint+'&scope='+customRequestedScope) 330 | .send(mockLoginPost) 331 | .end(function(err,res) { 332 | var access_token = res.headers['set-cookie'][1].match(/access_token=([^;]+)/)[1]; 333 | 334 | assert.equal(res.body.scope,customScope); 335 | parser.parseClaimsJws(access_token,function(err,jwt) { 336 | assert.equal(jwt.body.scope,customScope); 337 | done(); 338 | }); 339 | }); 340 | }); 341 | }); 342 | 343 | describe('and {writeAccessTokenResponse: true, writeAccessTokenToCookie: false} spConfig options',function(){ 344 | 345 | var app; 346 | 347 | before(function(done){ 348 | loginSuccessFixture(function(fixture){ 349 | var spMiddleware = stormpathSdkExpress.createMiddleware({ 350 | appHref: fixture.appHref, 351 | writeAccessTokenResponse: true, 352 | writeAccessTokenToCookie: false 353 | }); 354 | app = express(); 355 | app.use(bodyParser.json()); 356 | spMiddleware.attachDefaults(app); 357 | var wait = setInterval(function(){ 358 | /* wait for sp application */ 359 | if(spMiddleware.getApplication()){ 360 | clearInterval(wait); 361 | done(); 362 | } 363 | },100); 364 | }); 365 | }); 366 | 367 | it('should not write an access token cookie',function(done){ 368 | request(app) 369 | .post(tokenEndpoint) 370 | .send(mockLoginPost) 371 | .end(function(err,res){ 372 | assert(httpsOnlyCookieExpr.test(res.headers['set-cookie'].join(','))===false); 373 | done(); 374 | }); 375 | }); 376 | 377 | it('should write an access token tresponse body',function(done){ 378 | 379 | request(app) 380 | .post(tokenEndpoint) 381 | .send(mockLoginPost) 382 | .end(function(err,res){ 383 | assert(res.body.access_token.match(jwtExpr)); 384 | assert(res.body.token_type==='Bearer'); 385 | assert(res.body.expires_in===3600); 386 | done(); 387 | }); 388 | 389 | }); 390 | }); 391 | 392 | describe('and { writeAccessTokenToCookie: false} spConfig options',function(){ 393 | 394 | var app; 395 | 396 | before(function(done){ 397 | loginSuccessFixture(function(fixture){ 398 | var spMiddleware = stormpathSdkExpress.createMiddleware({ 399 | appHref: fixture.appHref, 400 | writeAccessTokenToCookie: false 401 | }); 402 | app = express(); 403 | app.use(bodyParser.json()); 404 | spMiddleware.attachDefaults(app); 405 | var wait = setInterval(function(){ 406 | /* wait for sp application */ 407 | if(spMiddleware.getApplication()){ 408 | clearInterval(wait); 409 | done(); 410 | } 411 | },100); 412 | }); 413 | }); 414 | 415 | it('should not write an access token cookie',function(done){ 416 | request(app) 417 | .post(tokenEndpoint) 418 | .send(mockLoginPost) 419 | .end(function(err,res){ 420 | assert(httpOnlyCookieExpr.test(res.headers['set-cookie'].join(','))===false); 421 | assert(httpsOnlyCookieExpr.test(res.headers['set-cookie'].join(','))===false); 422 | done(); 423 | }); 424 | }); 425 | 426 | it('should write an empty body with 201 response',function(done){ 427 | request(app) 428 | .post(tokenEndpoint) 429 | .send(mockLoginPost) 430 | .expect(201,'',done); 431 | }); 432 | }); 433 | 434 | 435 | }); 436 | 437 | 438 | 439 | }); -------------------------------------------------------------------------------- /test/cookie-options.js: -------------------------------------------------------------------------------- 1 | var bodyParser = require('body-parser'); 2 | var express = require('express'); 3 | var request = require('supertest'); 4 | 5 | var loginSuccessFixture = require('./fixtures/loginSuccess'); 6 | var properties = require('../properties.json'); 7 | 8 | describe('accessTokenCookieName option',function() { 9 | 10 | var tokenEndpoint = properties.configuration.DEFAULT_TOKEN_ENDPOINT + '?grant_type=password'; 11 | var customCookieName = 'yet-another-cookie'; 12 | var mockLoginPost = {username:'abc',password:'123'}; 13 | 14 | describe('when set to a custom value',function(){ 15 | var app; 16 | 17 | before(function(done){ 18 | loginSuccessFixture(function(fixture){ 19 | var spMiddleware = require('../').createMiddleware({ 20 | appHref: fixture.appHref, 21 | accessTokenCookieName: customCookieName 22 | }); 23 | app = express(); 24 | app.use(bodyParser.json()); 25 | spMiddleware.attachDefaults(app); 26 | 27 | var wait = setInterval(function(){ 28 | /* wait for sp application */ 29 | if(spMiddleware.getApplication()){ 30 | clearInterval(wait); 31 | done(); 32 | } 33 | },100); 34 | 35 | }); 36 | }); 37 | it('should be reflected in the cookie response as the default value',function(done){ 38 | request(app) 39 | .post(tokenEndpoint) 40 | .send(mockLoginPost) 41 | .expect('set-cookie', new RegExp(customCookieName),done); 42 | }); 43 | }); 44 | describe('when left as default',function(){ 45 | var app; 46 | 47 | before(function(done){ 48 | loginSuccessFixture(function(fixture){ 49 | var spMiddleware = require('../').createMiddleware({ 50 | appHref: fixture.appHref 51 | }); 52 | app = express(); 53 | app.use(bodyParser.json()); 54 | spMiddleware.attachDefaults(app); 55 | 56 | var wait = setInterval(function(){ 57 | /* wait for sp application */ 58 | if(spMiddleware.getApplication()){ 59 | clearInterval(wait); 60 | done(); 61 | } 62 | },100); 63 | 64 | }); 65 | }); 66 | it('should be reflected in the cookie response as the default value',function(done){ 67 | request(app) 68 | .post(tokenEndpoint) 69 | .send(mockLoginPost) 70 | .expect('set-cookie', new RegExp(properties.configuration.DEFAULT_ACCESS_TOKEN_COOKIE_NAME),done); 71 | }); 72 | }); 73 | }); -------------------------------------------------------------------------------- /test/cors.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stormpath/stormpath-sdk-express/cf2c66916eaea8154f7c3a1f2152f3004eabf3d4/test/cors.js -------------------------------------------------------------------------------- /test/custom-error-handlers.js: -------------------------------------------------------------------------------- 1 | var bodyParser = require('body-parser'); 2 | var express = require('express'); 3 | var request = require('supertest'); 4 | 5 | var loginSuccessFixture = require('./fixtures/loginSuccess'); 6 | var properties = require('../properties.json'); 7 | 8 | describe('endOnError option',function() { 9 | describe('when set to false',function(){ 10 | var app; 11 | var protectedUri = '/protected-resource'; 12 | before(function(done){ 13 | loginSuccessFixture(function(fixture){ 14 | var spMiddleware = require('../').createMiddleware({ 15 | appHref: fixture.appHref, 16 | endOnError: false 17 | }); 18 | app = express(); 19 | app.use(bodyParser.json()); 20 | spMiddleware.attachDefaults(app); 21 | app.get(protectedUri,spMiddleware.authenticate,function(req,res){ 22 | res.json({authenticationError:req.authenticationError.userMessage}); 23 | }); 24 | var wait = setInterval(function(){ 25 | /* wait for sp application */ 26 | if(spMiddleware.getApplication()){ 27 | clearInterval(wait); 28 | done(); 29 | } 30 | },100); 31 | }); 32 | }); 33 | it('should assign an authenticationError to the request',function(done){ 34 | request(app) 35 | .get(protectedUri) 36 | .expect(200,{authenticationError:properties.errors.authentication.UNAUTHENTICATED},done); 37 | }); 38 | }); 39 | describe('when left as default',function(){ 40 | var app; 41 | var protectedUri = '/protected-resource'; 42 | before(function(done){ 43 | loginSuccessFixture(function(fixture){ 44 | var spMiddleware = require('../').createMiddleware({ 45 | appHref: fixture.appHref 46 | }); 47 | app = express(); 48 | app.use(bodyParser.json()); 49 | app.use(spMiddleware.authenticate); 50 | var wait = setInterval(function(){ 51 | /* wait for sp application */ 52 | if(spMiddleware.getApplication()){ 53 | clearInterval(wait); 54 | done(); 55 | } 56 | },100); 57 | }); 58 | }); 59 | it('should end the response with the default error response',function(done){ 60 | request(app) 61 | .get(protectedUri) 62 | .expect(401,{errorMessage:properties.errors.authentication.UNAUTHENTICATED},done); 63 | }); 64 | }); 65 | }); -------------------------------------------------------------------------------- /test/email-verification-endpoints.js: -------------------------------------------------------------------------------- 1 | 2 | var request = require('supertest'); 3 | 4 | var properties = require('../properties.json'); 5 | var resentEndpoint = properties.configuration.RESEND_EMAIL_VERIFICATION_ENDPOINT; 6 | var verificationEndpoint = properties.configuration.EMAIL_VERIFICATION_TOKEN_COLLECTION_ENDPOINT; 7 | var helpers = require('./helpers'); 8 | 9 | 10 | describe('resend verification endpoint endpoint',function() { 11 | it('should add access control headers to OPTIONS responses, if the origin is whitelisted',function(done){ 12 | var origin = 'http://localhost:9000'; 13 | var app = helpers.buildApp({ 14 | allowedOrigins: [origin] 15 | }); 16 | request(app) 17 | .options(resentEndpoint) 18 | .set('Origin',origin) 19 | .expect('Access-Control-Allow-Origin',origin) 20 | .expect('Access-Control-Allow-Headers','Content-Type') 21 | .expect('Access-Control-Allow-Credentials','true') 22 | .expect(200,done); 23 | }); 24 | 25 | it('should not access control headers to OPTIONS responses for origins that are not in the whitelist',function(done){ 26 | helpers.getAppHref(function(appHref){ 27 | var app = helpers.buildApp({ 28 | appHref: appHref, 29 | allowedOrigins: ['a'] 30 | }); 31 | request(app) 32 | .options(resentEndpoint) 33 | .set('Origin','b') 34 | .expect(helpers.hasNullField('Access-Control-Allow-Origin')) 35 | .end(done); 36 | }); 37 | }); 38 | 39 | it('should not add access control headers to OPTIONS responses if there is no whitelist',function(done){ 40 | helpers.getAppHref(function(appHref){ 41 | var app = helpers.buildApp({ 42 | appHref: appHref 43 | }); 44 | request(app) 45 | .options(resentEndpoint) 46 | .expect(helpers.hasNullField('Access-Control-Allow-Origin')) 47 | .end(done); 48 | 49 | }); 50 | }); 51 | }); 52 | 53 | 54 | describe('token verification endpoint endpoint',function() { 55 | it('should add access control headers to OPTIONS responses, if the origin is whitelisted',function(done){ 56 | var origin = 'http://localhost:9000'; 57 | var app = helpers.buildApp({ 58 | allowedOrigins: [origin] 59 | }); 60 | request(app) 61 | .options(verificationEndpoint) 62 | .set('Origin',origin) 63 | .expect('Access-Control-Allow-Origin',origin) 64 | .expect('Access-Control-Allow-Headers','Content-Type') 65 | .expect('Access-Control-Allow-Credentials','true') 66 | .expect(200,done); 67 | }); 68 | 69 | it('should not access control headers to OPTIONS responses for origins that are not in the whitelist',function(done){ 70 | helpers.getAppHref(function(appHref){ 71 | var app = helpers.buildApp({ 72 | appHref: appHref, 73 | allowedOrigins: ['a'] 74 | }); 75 | request(app) 76 | .options(verificationEndpoint) 77 | .set('Origin','b') 78 | .expect(helpers.hasNullField('Access-Control-Allow-Origin')) 79 | .end(done); 80 | }); 81 | }); 82 | 83 | it('should not add access control headers to OPTIONS responses if there is no whitelist',function(done){ 84 | helpers.getAppHref(function(appHref){ 85 | var app = helpers.buildApp({ 86 | appHref: appHref 87 | }); 88 | request(app) 89 | .options(verificationEndpoint) 90 | .expect(helpers.hasNullField('Access-Control-Allow-Origin')) 91 | .end(done); 92 | 93 | }); 94 | }); 95 | }); 96 | -------------------------------------------------------------------------------- /test/fixtures/loginSuccess.js: -------------------------------------------------------------------------------- 1 | /* 2 | Returns an application at the given href 3 | 4 | Returns generic account objects when posting to the login attemps 5 | of the given app 6 | */ 7 | 8 | var bodyParser = require('body-parser'); 9 | var express = require('express'); 10 | var http = require('http'); 11 | var uuid = require('node-uuid'); 12 | 13 | function loginSuccessFixture2(done){ 14 | var app = express(); 15 | var server = http.createServer(app); 16 | 17 | 18 | server.listen(0,function(){ 19 | var base = 'http://0.0.0.0:'+server.address().port; 20 | var fixture = loginSuccessFixture(base,app); 21 | app.use(bodyParser.json()); 22 | done({appHref:fixture.appHref,accountHref:fixture.accountHref}); 23 | }); 24 | 25 | } 26 | 27 | 28 | function loginSuccessFixture(base,expressApp) { 29 | var appUri = '/v1/application/'+uuid(); 30 | var accountUri = '/v1/accounts/'+uuid(); 31 | 32 | var appHref = base+appUri; 33 | var loginAttemptsUri = appUri + '/loginAttempts'; 34 | var loginAttemptsHref = base + loginAttemptsUri; 35 | var accountHref = base+accountUri; 36 | var accountsUri = '/v1/applications/'+uuid() + '/accounts'; 37 | var accountsHref = base+accountsUri; 38 | 39 | function mockAccountResponse(req,res){ 40 | res.json({ 41 | href:accountsHref, 42 | status: "ENABLED" 43 | }); 44 | } 45 | 46 | expressApp.get(appUri,function(req,res){ 47 | res.json({href:appHref, 48 | loginAttempts:{href:loginAttemptsHref}, 49 | accounts:{href:accountsHref}}); 50 | }); 51 | 52 | expressApp.get(accountUri,mockAccountResponse); 53 | expressApp.post(accountsUri,mockAccountResponse); 54 | 55 | expressApp.post(loginAttemptsUri,function(req,res){ 56 | res.json({ 57 | account: { 58 | href: accountHref 59 | } 60 | }); 61 | }); 62 | 63 | return { 64 | appHref: appHref, 65 | accountHref: accountHref 66 | }; 67 | } 68 | module.exports = loginSuccessFixture2; -------------------------------------------------------------------------------- /test/helpers.js: -------------------------------------------------------------------------------- 1 | var bodyParser = require('body-parser'); 2 | var express = require('express'); 3 | var loginSuccessFixture = require('./fixtures/loginSuccess'); 4 | 5 | function hasNullField(field){ 6 | return function(res){ 7 | var value = res.headers[field.toLowerCase()]; 8 | return value ? ("Expected header '" + field + "' to be null, but has value '"+value+"'") : false; 9 | }; 10 | } 11 | 12 | function getAppHref(next){ 13 | loginSuccessFixture(function(fixture){ 14 | var spMiddleware = require('../').createMiddleware({ 15 | appHref: fixture.appHref, 16 | allowedOrigins: ['a'] 17 | }); 18 | var app = express(); 19 | app.use(bodyParser.json()); 20 | spMiddleware.attachDefaults(app); 21 | 22 | var wait = setInterval(function(){ 23 | /* wait for sp application */ 24 | if(spMiddleware.getApplication()){ 25 | clearInterval(wait); 26 | next(fixture.appHref); 27 | } 28 | },100); 29 | }); 30 | } 31 | 32 | 33 | function buildApp(spConfig){ 34 | var spMiddleware = require('../').createMiddleware(spConfig); 35 | var app = express(); 36 | app.use(bodyParser.json()); 37 | spMiddleware.attachDefaults(app); 38 | return app; 39 | } 40 | 41 | module.exports = { 42 | hasNullField: hasNullField, 43 | getAppHref: getAppHref, 44 | buildApp: buildApp 45 | }; -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var express = require('express'); 3 | var http = require('http'); 4 | var request = require('supertest'); 5 | 6 | var pkg = require('../package.json'); 7 | var properties = require('../properties'); 8 | var stormpathSdkExpress = require('../'); 9 | 10 | /* 11 | This library creates self-signed certificates in order to test 12 | the HTTPS features of the library. 13 | 14 | At the time of writing there is a problem with supertest and 15 | self-signed certificates. It failes on the Certificate Authority 16 | mismtach. 17 | 18 | There an option that has been merged into the underlying 19 | superagent module, but I was not able to use it successfully 20 | 21 | See: 22 | https://github.com/visionmedia/superagent/issues/197 23 | https://github.com/visionmedia/superagent/pull/198 24 | 25 | The workaround is to set NODE_TLS_REJECT_UNAUTHORIZED = 0 26 | 27 | I don't like this because it also means we don't validate 28 | security on requests to api.stormpath.com in our IT tests 29 | */ 30 | 31 | process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; 32 | 33 | 34 | describe('the user agent of this library',function(){ 35 | 36 | var expectedUaExpr = '^stormpath-sdk-express\/'+pkg.version; 37 | 38 | var app, appHref, ua; 39 | 40 | before(function(done){ 41 | 42 | /* 43 | Create a mock API service and pass a mock application href 44 | to the express app. When the express app calls that mock href 45 | we will inpsect the user agent to ensure that it's being 46 | sent corectly 47 | */ 48 | 49 | var mockApiServer = http.createServer(function(req,res){ 50 | ua = req.headers['user-agent']; 51 | res.end(JSON.stringify({userAgent:req.headers && req.headers['user-agent']})); 52 | }).listen(0,function(){ 53 | appHref = 'http://0.0.0.0:'+mockApiServer.address().port+'/an-application'; 54 | app = express(); 55 | stormpathSdkExpress.createMiddleware({ 56 | appHref: appHref 57 | }); 58 | }); 59 | 60 | 61 | var wait = setInterval(function(){ 62 | /* wait for the library to make an api call */ 63 | if(ua){ 64 | clearInterval(wait); 65 | done(); 66 | } 67 | },100); 68 | 69 | }); 70 | it('should reflect the user agent of this module',function(){ 71 | assert(new RegExp(expectedUaExpr).test(ua),'user agent is not correct'); 72 | }); 73 | }); 74 | 75 | describe('createMiddleware',function(){ 76 | 77 | var a,b,c; 78 | before(function(){ 79 | a = process.env.STORMPATH_API_KEY_SECRET; 80 | b = process.env.STORMPATH_API_KEY_ID; 81 | c = process.env.STORMPATH_APP_HREF; 82 | delete process.env.STORMPATH_API_KEY_SECRET; 83 | delete process.env.STORMPATH_API_KEY_ID; 84 | delete process.env.STORMPATH_APP_HREF; 85 | }); 86 | after(function(){ 87 | process.env.STORMPATH_API_KEY_SECRET = a; 88 | process.env.STORMPATH_API_KEY_ID = b; 89 | process.env.STORMPATH_APP_HREF = c; 90 | }); 91 | it('should throw if an api key ID is not given',function(){ 92 | assert.throws(function(){ 93 | stormpathSdkExpress.createMiddleware({}); 94 | },properties.errors.MISSING_API_KEY_ID); 95 | }); 96 | it('should throw if an api key secret is not given',function(){ 97 | assert.throws(function(){ 98 | stormpathSdkExpress.createMiddleware({ 99 | apiKeyId: '1' 100 | }); 101 | },properties.errors.MISSING_API_KEY_SECRET); 102 | }); 103 | it('should throw if an app href is not given',function(){ 104 | assert.throws(function(){ 105 | stormpathSdkExpress.createMiddleware({ 106 | apiKeyId: '1', 107 | apiKeySecret: '1' 108 | }); 109 | },properties.errors.MISSING_APP_HREF); 110 | }); 111 | 112 | it('should expose the stormpath client for use',function(){ 113 | var spMiddleware = stormpathSdkExpress.createMiddleware({ 114 | apiKeyId: '1', 115 | apiKeySecret: '1', 116 | appHref: 'x' 117 | }); 118 | assert(spMiddleware.spClient); 119 | assert(spMiddleware.spClient.getCurrentTenant); 120 | }); 121 | }); 122 | 123 | 124 | describe('default middleware from createMiddleware() with default options',function(){ 125 | var stormpathMiddleware, app; 126 | 127 | var spConfig = { 128 | apiKeyId:'1', 129 | apiKeySecret:'2', 130 | appHref: '', 131 | stormpath: { 132 | // Mock out the stormpath library, we don't need to get a client 133 | // or api key for this test 134 | Client: function(){return { 135 | getApplication:function(){ 136 | 137 | }, 138 | getCurrentTenant: function(cb){ 139 | cb(null,undefined); 140 | }}; 141 | }, 142 | ApiKey: function(){}, 143 | } 144 | }; 145 | 146 | before(function(){ 147 | stormpathMiddleware = stormpathSdkExpress.createMiddleware(spConfig); 148 | app = express(); 149 | app.use(stormpathMiddleware); 150 | }); 151 | 152 | describe('when passed to app.use()',function(){ 153 | it('should respond to POST token requests at the default token endpoint',function(done){ 154 | request(app) 155 | .post(properties.configuration.DEFAULT_TOKEN_ENDPOINT) 156 | .expect(401,{errorMessage:properties.errors.authentication.UNSUPPORTED_GRANT_TYPE},done); 157 | }); 158 | it('should reject GET token requests at the default token endpoint',function(done){ 159 | request(app) 160 | .get(properties.configuration.DEFAULT_TOKEN_ENDPOINT) 161 | .expect(405,done); 162 | }); 163 | it('should attempt to authenticate all other requests',function(done){ 164 | request(app) 165 | .get('/something-else') 166 | .expect(401,{errorMessage:properties.errors.authentication.UNAUTHENTICATED},done); 167 | }); 168 | }); 169 | }); 170 | 171 | describe('tokenExchange middleware',function(){ 172 | 173 | describe('when constructed with a custom error handler',function(){ 174 | describe('and passed a post body with an invalid grant_type',function(){ 175 | it('call the custom error handler'); 176 | }); 177 | describe('and passed a post body with invalid credentials',function(){ 178 | it('call the custom error handler'); 179 | }); 180 | }); 181 | 182 | describe('when constructed with a scope handler',function(){ 183 | describe('and passed a post body with grant_type=password, valid username and password, and scope request',function(){ 184 | it('should call the scope handler with the resolved account and a callback which expects a string and will continue with the token exchange'); 185 | }); 186 | }); 187 | }); 188 | 189 | 190 | describe('authenticate() middleware',function(){ 191 | 192 | // todo validate scope of token 193 | 194 | describe('when constructed with a custom error handler',function(){ 195 | 196 | describe('and passed a username and password as json POST',function(){ 197 | describe('and the credentials are invalid',function(){ 198 | it('call the custom error handler'); 199 | }); 200 | }); 201 | describe('and passed a username and password as a multipart form post',function(){ 202 | describe('and the credentials are invalid',function(){ 203 | it('call the custom error handler'); 204 | }); 205 | }); 206 | }); 207 | 208 | }); 209 | -------------------------------------------------------------------------------- /test/it-fixtures/README.md: -------------------------------------------------------------------------------- 1 | This folder is where you should place the data that is required to run 2 | itegration tests against the API. Each expected file is documented here: 3 | 4 | ### apiAuth.json 5 | 6 | This file provides data that is required for testing the API auth features. 7 | It needs a reference to: 8 | 9 | * An enabled application 10 | * An enabled account that is reacable by that application 11 | * An enabled API key for that account 12 | 13 | **Example**: 14 | ```javascript 15 | { 16 | "apiKeyId": "xxx", 17 | "apiKeySecret": "xxx", 18 | "appHref": "https://api.stormpath.com/v1/applications/xxx", 19 | "accountHref": "https://api.stormpath.com/v1/accounts/xxx", 20 | "accountApiKeyId": "xxx", 21 | "accountApiKeySecret": "xxx" 22 | } 23 | ``` 24 | 25 | 26 | ### loginAuth.json 27 | 28 | This file provides data that is required for testing the username & password 29 | authentication features 30 | 31 | * An enabled application 32 | * An enabled account that is reacable by that application 33 | * The username and password for that account 34 | 35 | **Example**: 36 | ```javascript 37 | { 38 | "apiKeyId": "xxx", 39 | "apiKeySecret": "xxx", 40 | "appHref": "https://api.stormpath.com/v1/applications/xxx", 41 | "accountHref": "https://api.stormpath.com/v1/accounts/xxx", 42 | "accountUsername": "xxx", 43 | "accountPassword": "xxxM" 44 | } 45 | ``` -------------------------------------------------------------------------------- /test/it-fixtures/loader.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var path = require('path'); 3 | 4 | module.exports = function(fixtureName) { 5 | 6 | var fixturePath = path.join(__dirname,fixtureName); 7 | 8 | if(fs.existsSync(fixturePath)){ 9 | return require(fixturePath); 10 | }else{ 11 | throw new Error('Fixture file does not exist: '+fixturePath); 12 | } 13 | }; -------------------------------------------------------------------------------- /test/password-reset-endpoint.js: -------------------------------------------------------------------------------- 1 | 2 | var request = require('supertest'); 3 | 4 | var properties = require('../properties.json'); 5 | var endpoint = properties.configuration.PASSWORD_RESET_TOKEN_COLLECTION_ENDPOINT; 6 | 7 | var helpers = require('./helpers'); 8 | 9 | 10 | describe('password reset token endpoint',function() { 11 | it('should add access control headers to OPTIONS responses, if the origin is whitelisted',function(done){ 12 | var origin = 'http://localhost:9000'; 13 | var app = helpers.buildApp({ 14 | allowedOrigins: [origin] 15 | }); 16 | request(app) 17 | .options(endpoint) 18 | .set('Origin',origin) 19 | .expect('Access-Control-Allow-Origin',origin) 20 | .expect('Access-Control-Allow-Headers','Content-Type') 21 | .expect('Access-Control-Allow-Credentials','true') 22 | .expect(200,done); 23 | 24 | }); 25 | 26 | it('should not access control headers to OPTIONS responses for origins that are not in the whitelist',function(done){ 27 | helpers.getAppHref(function(appHref){ 28 | var app = helpers.buildApp({ 29 | appHref: appHref, 30 | allowedOrigins: ['a'] 31 | }); 32 | request(app) 33 | .options(endpoint) 34 | .set('Origin','b') 35 | .expect(helpers.hasNullField('Access-Control-Allow-Origin')) 36 | .end(done); 37 | }); 38 | }); 39 | 40 | it('should not add access control headers to OPTIONS responses if there is no whitelist',function(done){ 41 | helpers.getAppHref(function(appHref){ 42 | var app = helpers.buildApp({ 43 | appHref: appHref 44 | }); 45 | request(app) 46 | .options(endpoint) 47 | .expect(helpers.hasNullField('Access-Control-Allow-Origin')) 48 | .end(done); 49 | 50 | }); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /test/token-endpoint.js: -------------------------------------------------------------------------------- 1 | 2 | var request = require('supertest'); 3 | 4 | var properties = require('../properties.json'); 5 | var tokenEndpoint = properties.configuration.DEFAULT_TOKEN_ENDPOINT + '?grant_type=password'; 6 | var helpers = require('./helpers'); 7 | 8 | 9 | describe('oauth token endpoint',function() { 10 | it('should add access control headers to OPTIONS responses, if the origin is whitelisted',function(done){ 11 | var origin = 'http://localhost:9000'; 12 | var app = helpers.buildApp({ 13 | allowedOrigins: [origin] 14 | }); 15 | request(app) 16 | .options(tokenEndpoint) 17 | .set('Origin',origin) 18 | .expect('Access-Control-Allow-Origin',origin) 19 | .expect('Access-Control-Allow-Headers','Content-Type') 20 | .expect('Access-Control-Allow-Credentials','true') 21 | .expect(200,done); 22 | }); 23 | 24 | it('should not access control headers to OPTIONS responses for origins that are not in the whitelist',function(done){ 25 | helpers.getAppHref(function(appHref){ 26 | var app = helpers.buildApp({ 27 | appHref: appHref, 28 | allowedOrigins: ['a'] 29 | }); 30 | request(app) 31 | .options(tokenEndpoint) 32 | .set('Origin','b') 33 | .expect(helpers.hasNullField('Access-Control-Allow-Origin')) 34 | .end(done); 35 | }); 36 | }); 37 | 38 | it('should not add access control headers to OPTIONS responses if there is no whitelist',function(done){ 39 | helpers.getAppHref(function(appHref){ 40 | var app = helpers.buildApp({ 41 | appHref: appHref 42 | }); 43 | request(app) 44 | .options(tokenEndpoint) 45 | .expect(helpers.hasNullField('Access-Control-Allow-Origin')) 46 | .end(done); 47 | 48 | }); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /test/write-tokens-option.js: -------------------------------------------------------------------------------- 1 | var bodyParser = require('body-parser'); 2 | var express = require('express'); 3 | var request = require('supertest'); 4 | 5 | var loginSuccessFixture = require('./fixtures/loginSuccess'); 6 | var properties = require('../properties.json'); 7 | 8 | describe('writeTokens option',function() { 9 | 10 | var tokenEndpoint = properties.configuration.DEFAULT_TOKEN_ENDPOINT + '?grant_type=password'; 11 | var mockLoginPost = {username:'abc',password:'123'}; 12 | 13 | 14 | describe('when set to false',function(){ 15 | var app, accountHref; 16 | 17 | before(function(done){ 18 | loginSuccessFixture(function(fixture){ 19 | accountHref = fixture.accountHref; 20 | var spMiddleware = require('../').createMiddleware({ 21 | appHref: fixture.appHref, 22 | writeTokens: false 23 | }); 24 | app = express(); 25 | app.use(bodyParser.json()); 26 | app.post(properties.configuration.DEFAULT_TOKEN_ENDPOINT,spMiddleware.authenticateForToken,function(req,res){ 27 | res.json({accountHref:req.authenticationResult.account.href}); 28 | }); 29 | 30 | var wait = setInterval(function(){ 31 | /* wait for sp application */ 32 | if(spMiddleware.getApplication()){ 33 | clearInterval(wait); 34 | done(); 35 | } 36 | },100); 37 | 38 | }); 39 | }); 40 | it('should set an authenticationResult on the request',function(done){ 41 | request(app) 42 | .post(tokenEndpoint) 43 | .send(mockLoginPost) 44 | .expect(200,{accountHref:accountHref},done); 45 | }); 46 | }); 47 | describe('when left as default',function(){ 48 | var app; 49 | var httpOnlyCookieExpr = /access_token=[^\.]+\.[^\.]+\.[^;]+; Expires=[^;]+; HttpOnly;/; 50 | 51 | before(function(done){ 52 | loginSuccessFixture(function(fixture){ 53 | var spMiddleware = require('../').createMiddleware({ 54 | appHref: fixture.appHref 55 | }); 56 | app = express(); 57 | app.use(bodyParser.json()); 58 | spMiddleware.attachDefaults(app); 59 | 60 | var wait = setInterval(function(){ 61 | /* wait for sp application */ 62 | if(spMiddleware.getApplication()){ 63 | clearInterval(wait); 64 | done(); 65 | } 66 | },100); 67 | 68 | }); 69 | }); 70 | it('should write tokens on the response',function(done){ 71 | request(app) 72 | .post(tokenEndpoint) 73 | .send(mockLoginPost) 74 | .expect('set-cookie', httpOnlyCookieExpr) 75 | .expect(201,'',done); 76 | }); 77 | }); 78 | }); --------------------------------------------------------------------------------