├── .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 | [](https://npmjs.org/package/stormpath-sdk-express)
9 | [](https://npmjs.org/package/stormpath-sdk-express)
10 | [](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 | });
--------------------------------------------------------------------------------