├── .dockerignore
├── .gitignore
├── .npmrc
├── .nycrc
├── .travis.yml
├── Dockerfile
├── Dockerfile.prod
├── Jenkinsfile
├── LICENSE
├── Logo.png
├── README.md
├── apiary.apib
├── build.sh
├── codeship-services.yml
├── codeship-steps.yml
├── deployment.env.encrypted
├── docker-compose.prod.yml
├── docker-compose.yml
├── docs
└── index.html
├── package.json
├── src
├── bin
│ └── www.ts
├── config.ts
├── controllers
│ ├── methods.ts
│ ├── specs
│ │ ├── methods.spec.ts
│ │ └── verifiers.spec.ts
│ └── verifiers.ts
├── exceptions
│ └── exceptions.ts
├── helpers
│ ├── responses.ts
│ └── specs
│ │ └── responses.spec.ts
├── index.d.ts
├── ioc.container.ts
├── middlewares
│ ├── common.ts
│ ├── requests.ts
│ └── specs
│ │ ├── common.spec.ts
│ │ └── requests.spec.ts
├── server.ts
└── services
│ ├── auth.ts
│ ├── authenticator.verification.ts
│ ├── base.verification.ts
│ ├── email.verification.ts
│ ├── helpers.ts
│ ├── providers
│ ├── email.ts
│ ├── index.ts
│ └── specs
│ │ └── email.spec.ts
│ ├── specs
│ ├── auth.spec.ts
│ ├── authenticator.spec.ts
│ ├── storages.spec.ts
│ └── verifications.spec.ts
│ ├── storages.ts
│ └── verifications.ts
├── tsconfig.build.json
├── tsconfig.json
├── tslint.json
└── yarn.lock
/.dockerignore:
--------------------------------------------------------------------------------
1 | .git
2 | node_modules
3 | npm-debug.log
4 | coverage
5 | .nyc_output
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | npm-debug.log
3 | coverage
4 | .nyc_output
5 | dist
6 | ts-node
7 | .idea
8 | .vscode
9 |
10 | codeship.aes
11 | deployment.env
12 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | save-exact=true
2 |
--------------------------------------------------------------------------------
/.nycrc:
--------------------------------------------------------------------------------
1 | {
2 | "lines": 80,
3 | "statements": 80,
4 | "functions": 80,
5 | "branches": 78,
6 | "include": [
7 | "**/*.ts"
8 | ],
9 | "exclude": [
10 | "**/*.spec.ts",
11 | "coverage",
12 | "*.ts",
13 | "src/*.ts"
14 | ],
15 | "reporter": [
16 | "html",
17 | "text",
18 | "text-summary"
19 | ],
20 | "require": [
21 | "ts-node/register"
22 | ],
23 | "extension": [
24 | ".ts"
25 | ],
26 | "cache": false,
27 | "all": false,
28 | "check-coverage": true
29 | }
30 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: required
2 | services:
3 | - docker
4 | before_install:
5 | - docker login -u $DOCKER_USER -p $DOCKER_PASS
6 | - chmod ugo+x ./build.sh
7 | script:
8 | - docker-compose build verify
9 | - docker-compose run --rm verify sh -c "yarn && yarn test"
10 | - export TAG=`if [ "$TRAVIS_BRANCH" == "master" ]; then echo "production"; elif ["$TRAVIS_BRANCH"
11 | == "develop"]; then echo "stage"; else echo "dev-$(git rev-parse --short HEAD)"
12 | ; fi`
13 | - "./build.sh $TAG"
14 | env:
15 | global:
16 | - secure: iav2owjHUd8+tPlyX+mzHmVayZJoGilep3A8/EvJIvb1t6Do39tQU4lSqd0LhJxg3mQh6kV2oBk0OZO8LInSD/XrGn1afuqWdPCNd02GbdqyBqZ5NiazpzL9mpiL8tAMQsfBtSe5sAkg47e9URzoUpXLNhg/6VD3iPqwBGOJ1eLvxiziotVeKlB+RweEUUCb6x/Mo4iEY0BRI+ciYW1pNXtKwHGYg35lxbG5vjJVl4p9LN0+J6D5gc9COHQNSqZDA3Y6CY3/Nxr6F/pHqgl5VycRXaGg4PcmnnB7RfaSgp/10x6pN1VB2pUcIj6d9JmdNKUPswX/E6b068IIuCHpuVw5BKKD/qGzLZufR+dLViFQjkZb9ZNXNbWpbyEH7JGewc7hkOPFDC9TW5Mn64SLRzs9IMzN2eEL3pkW0Tg36i1N/NTevZnxIsRGbRyLBYn4VItVe5eHQOheBTPmJ+Txb82cdPLhSv0WjvQtp25tAy34tfQVAoZ8Tiq07oTTFM41UTJhVLQCKp7bEO0MNUQzfXjpPZWyFsnyhn9ZBy9MJum4Ud41tmvedYVzDjzAKnnNXJlMpPdwb3nj2czYly2Man9wRbkSLKa7dnv08v2Y5I+UuVUZ7sgtjKH9OSghhvyjNvtR70YX6J6nBNPGnOZwNAuCM7Rq0JYuoRzh8fkWtUY=
17 | - secure: gtld4jpjYEo+gfOOtwp89zTFPfHx6A42+NGAB+RyABgZqDdqqyBKSo/fjsIgy4etSiZ6yZhoqrAjzon1oe6YJsHInDpnuPzpSdnpUg6YsJFusHf+mPEKg/dPVS4dPA/bjPYcCgK7Cys+YUKeMKjUqyufv+duufjL89TOdpTMvhqnepHc9N/R75AX7kKR4f1kGR1ce3m48QAidgq/m5j4u7Nkl+5qvmtyOd9SULyiGFxvbckBGzhXRpmlIXakqZa90ui3pirfeJHq5S2s6x9hrV6i3MYWxsRmeoEOpCT40W/wEZwVvMo3zTAVmZLsl9QvMhdtjwYKBgRC0dd3gFLYV8szGsxU9qDnOaCPleawE3SZ/+4WvHPxpN/bzQdRDFJLexhMhydmZnEWg2DpFyow6Oz4f/ViPqH3qySkwEUX5EBmX1pTk5nFpdtzfTN3FCUb3sYSlEbBopsU8KqA5hRnflB5b1JwoN4vg/pUBt/8f2Fi8lbxtkB9NbHmQvSufVAnluCOFlC5AkzHQZP3xSL3+7vIYVPHKGru6qqwa2yZXvJdLNspZO+WN6NIRK14NpaptPUmmo+fGpjUdyKXo5a/WH/NAGGcUPT1N0fVOqwbE2+K8PJVwNN+GoCv1KUS/U6C3AQlb0vbv8wqAL0Ud9pzkD9PRs7O+Fpmv/Hmxfyz86o=
18 | notifications:
19 | slack:
20 | secure: XkpxurHZJxQuhwnAnQvhWEIIOJGLywHQgW+DLjNCxwo4nM82IbsTY4a6JicsO0jkzdkxX1yk5W4pB7BbBeiuwLC72gRZGmD2AY1EZThsPsVtv6aMi6VXgYyiAgAv2gV8Dv7P8MsHnSiqkwuIwHnveu3QA2yTjnsDeyfsMWRSdH1C4kb6EpsxMV2LE0KxEtM88Vney2Qm82LhSyB0x9c2ExVsQOOtseKMaeA2KXgm0b2wJLmYgtapk8eZszC9fao4ANkCfVvipt78C4eJvska/0KGem36k7ZKZm+Qgk8SUWU1YHALOznsCzuVKErwuaurDxgFcImxQgGdQ2+ygHQPqdh16i0kmEu/TPb1IxKSf8a2NBUfoSutneDmrJAD9peEFnXvAA2D8RICtLMkxprXgPgncEwRlPdfpMI/F7xv/+1u2xWjUrSME2IjHFUACLbdnrKH6VSnCrXZmKTLC0+nsujAEPELqSWfJ3xzNReKf6XqCADVSm4rwvbaV9E4YlFNiPKnQTpV3ELe0hEcouI4kKAYWR59g6QLjYacVvosK2Gv0q8c9jo67X1Tf3E4gOiN7Ai3qjKSwlE3QXaQzlHPEzzje0ImdT39F2dCTdydTgh6PX23B6IIfHwZE1PGn8oHwtpZ/kZ9K739W7TwAkbW6Bv94uXrGTOlQGn0iMBZIAY=
21 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM mhart/alpine-node:8.9.1
2 |
3 | VOLUME /usr/src/app
4 | WORKDIR /usr/src/app
5 | EXPOSE 3000
6 | EXPOSE 4000
7 |
--------------------------------------------------------------------------------
/Dockerfile.prod:
--------------------------------------------------------------------------------
1 | FROM mhart/alpine-node:8.9.1
2 |
3 | RUN apk update && apk add bash && rm -rf /var/cache/apk/*
4 |
5 | RUN mkdir -p /usr/src/app
6 | ADD . /usr/src/app
7 |
8 | WORKDIR /usr/src/app
9 |
10 | RUN yarn
11 | RUN mkdir ./dist
12 | RUN npm run build
13 |
14 | CMD npm run serve
15 |
--------------------------------------------------------------------------------
/Jenkinsfile:
--------------------------------------------------------------------------------
1 | pipeline {
2 | agent any
3 | stages {
4 | stage('Build') {
5 | steps {
6 | sh 'docker-compose build -f docker-compose.prod.yml'
7 | }
8 | }
9 | stage('Deploy') {
10 | steps {
11 | sh 'docker-compose push -f docker-compose.prod.yml'
12 | }
13 | }
14 | }
15 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Jincor
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/secret-tech/backend-verify/8be9c7b7cf771e86f78cc79b81dc5a48c27f2973/Logo.png
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Jincor Verification Service
2 | 
3 | 
4 |
5 | # Jincor VERIFY Service
6 |
7 | Jincor Verification is a service for verify users email, phone, and etc.
8 | The main responsibilities are:
9 | 1. Interact with a service provider
10 | 1. Validation of a received code
11 |
12 | Take a look at our [Wiki](../../wiki) for more details.
13 |
14 | ## API Endpoints Summary
15 | For more details see the [API docs](https://jincortech.github.io/backend-verify/)
16 |
17 | JWT_TOKEN should be passed for every API call in the HTTP headers,
18 | that was received from auth service.
19 |
20 | 1. `/methods/{METHOD}/actions/initiate [POST]`
21 | 1. `/methods/{METHOD}/verifiers/{VERIFICATION_ID}/actions/validate [POST]`
22 | 1. `/methods/{METHOD}/verifiers/{VERIFICATION_ID} [DELETE]`
23 |
24 | ## How to start development and run tests?
25 |
26 | 1. Clone this repo.
27 | 1. Run `docker-compose build --no-cache`.
28 | 1. Run `docker-compose up -d`.
29 | 1. To install dependencies run: `docker-compose exec verify yarn`.
30 | 1. To run tests run: `docker-compose exec verify yarn test`.
31 | 1. To build production image run `docker-compose -f docker-compose.prod.yml build --no-cache`.
32 |
33 | ## How to generate docs?
34 | 1. Install `npm install -g aglio`.
35 | 1. Run `mkdir /usr/local/lib/node_modules/aglio/node_modules/aglio-theme-olio/cache`.
36 | 1. Generate `aglio --theme-variables cyborg --theme-template triple -i apiary.apib -o ./docs/index.html`.
--------------------------------------------------------------------------------
/apiary.apib:
--------------------------------------------------------------------------------
1 | FORMAT: 1A
2 | HOST: http://verify:3000/
3 |
4 | # Jincor VERIFY Service
5 |
6 | Jincor Verification is a service for verify users email, phone, and etc.
7 | The main responsibilities are:
8 | 1. Interact with a service provider
9 | 1. Validation of a received code
10 |
11 | # API Endpoints Summary
12 |
13 | JWT_TOKEN should be passed for every API call in the HTTP headers,
14 | that was received from auth service.
15 |
16 | 1. `/methods/{METHOD}/actions/initiate [POST]`
17 | 1. `/methods/{METHOD}/verifiers/{VERIFICATION_ID}/actions/validate [POST]`
18 | 1. `/methods/{METHOD}/verifiers/{VERIFICATION_ID} [DELETE]`
19 |
20 | # Notes about google_auth
21 | 1. When you initiate google_auth verification 1st time for selected consumer you will get `totpUri` to show QR code in your frontend app.
22 | 2. The received secret will be **TEMPORARY** until you verify it first time. If secret is not verified at least 1 time and you `initiate` new google_auth verification for consumer - you will get **NEW** secret. It's required to ensure that user successfully stored the secret and entered correct code.
23 | 3. When you want to disable 2FA - initiate verification and send `removeSecret=true` param to `validate` endpoint to remove consumer's secret.
24 |
25 | ## Verification: Initiate [/methods/{METHOD}/actions/initiate]
26 |
27 | Initiate verification process, with usage of specified *METHOD*.
28 |
29 | + Parameters
30 | + METHOD (string) - One of *email*, *google_auth*, *phone* (not implemented).
31 |
32 | ### Initiate verification [POST]
33 |
34 | Example: email verification `/methods/email/actions/initiate`.
35 | Pass uuid in `policy.forcedVerificationId` to force using of your verification generated id.
36 | Set up own code in `policy.forcedCode` to force using of your verification code (does not apply for google_auth method).
37 |
38 | + Request (application/json)
39 | + Headers
40 |
41 | Authorization: Bearer {JWT_TOKEN}
42 | Accept: application/vnd.jincor+json; version=1
43 |
44 | + Body
45 |
46 | {
47 | "consumer": "test@test.com",
48 | "issuer": "Jincor",
49 | "template": {
50 | "body": "Click on the Verify Link to continue registration."
51 | },
52 | "generateCode": {
53 | "length": 32,
54 | "symbolSet": ["DIGITS", "alphas", "ALPHAS"]
55 | },
56 | "policy": {
57 | "expiredOn": "01:00:00"
58 | },
59 | "payload": {
60 | "your": "custom payload"
61 | }
62 | }
63 |
64 | + Response 200 (application/json)
65 |
66 | {
67 | "status": 200,
68 | "verificationId": "dc910ae0-7c67-4ace-8ebb-9edd4b5d8b0f",
69 | "attempts": 0,
70 | "expiredOn": 1505817462,
71 | "payload": {
72 | "your": "custom payload"
73 | }
74 | }
75 |
76 | + Response 200 (application/json)
77 |
78 | {
79 | "verificationId": "5028c0cd-07a9-4fa7-8d88-49edd2a44b72",
80 | "consumer": "test@test.com",
81 | "expiredOn": 1508689744,
82 | "totpUri": "otpauth://totp/:test@test.com?secret=CK53DOA3R7B2ZDMZKVOM53ZPT355ORJI&issuer=&algorithm=SHA1&digits=6&period=30",
83 | "status": 200
84 | }
85 |
86 | + Response 404 (application/json)
87 |
88 | {
89 | "status": 404,
90 | "error": "Method not supported"
91 | }
92 |
93 | + Response 422 (application/json)
94 |
95 | {
96 | "status": 422,
97 | "error": "Invalid request",
98 | "details": [
99 | {"path": "generateCode.length", "error": "Incorrect number format"}
100 | ]
101 | }
102 |
103 |
104 | ## Verification: Validate [/methods/{METHOD}/verifiers/{VERIFICATION_ID}/actions/validate]
105 |
106 | + Parameters
107 | + METHOD (string) - One of *phone*, *email*, *google_auth*.
108 | + VERIFICATION_ID (string)
109 |
110 | ### Validate the code [POST]
111 |
112 | Example: code validation for the email
113 | method `/methods/email/verifiers/dc910ae0-7c67-4ace-8ebb-9edd4b5d8b0f/actions/validate`.
114 |
115 | + code `1234qwertA` (required)
116 | + removeSecret `true` (optional - use it to remove secret when you want to disable 2FA for consumer)
117 |
118 | + Request (application/json)
119 |
120 | + Headers
121 |
122 | Authorization: Bearer {JWT_TOKEN}
123 | Accept: application/vnd.jincor+json; version=1
124 |
125 | + Body
126 |
127 | {
128 | "code": "JeDknKO0EZRBT6aFPrFQhzcCA2aqyVsHzZeJ8Vf",
129 | "removeSecret": true
130 | }
131 |
132 | + Response 200 (application/json)
133 |
134 | {
135 | "status": 200,
136 | "data": {
137 | "verificationId": "dc910ae0-7c67-4ace-8ebb-9edd4b5d8b0f",
138 | "consumer": "test@test.com",
139 | "expiredOn": 1505817462,
140 | "payload": {
141 | "your": "custom payload"
142 | },
143 | "attempts": 0
144 | }
145 | }
146 |
147 | + Response 404 (application/json)
148 |
149 | {
150 | "status": 404,
151 | "error": "Not found"
152 | }
153 |
154 | + Response 422 (application/json)
155 |
156 | {
157 | "status": 422,
158 | "error": "Invalid code",
159 | "data": {
160 | "verificationId": "dc910ae0-7c67-4ace-8ebb-9edd4b5d8b0f",
161 | "consumer": "test@test.com",
162 | "expiredOn": 1505817462,
163 | "payload": {
164 | "your": "custom payload"
165 | },
166 | "attempts": 1
167 | }
168 | }
169 |
170 |
171 | ## Verifications [/methods/{METHOD}/verifiers/{VERIFICATION_ID}]
172 |
173 | + Parameters
174 | + METHOD (string) - One of *phone*, *email*, *google_auth*.
175 | + VERIFICATION_ID (string)
176 |
177 | ### Get verification [GET]
178 |
179 | + Request (application/json)
180 |
181 | + Headers
182 |
183 | Authorization: Bearer {JWT_TOKEN}
184 | Accept: application/vnd.jincor+json; version=1
185 |
186 | + Response 200 (application/json)
187 |
188 | {
189 | "status": 200,
190 | "data": {
191 | "verificationId": "dc910ae0-7c67-4ace-8ebb-9edd4b5d8b0f",
192 | "consumer": "test@test.com",
193 | "expiredOn": 1505817462,
194 | "payload": {
195 | "your": "custom payload"
196 | },
197 | "attempts": 1
198 | }
199 | }
200 |
201 | + Response 404 (application/json)
202 |
203 | {
204 | "status": 404,
205 | "error": "Not found"
206 | }
207 |
208 | ### Invalidate the code [DELETE]
209 |
210 | + Request (application/json)
211 |
212 | + Headers
213 |
214 | Authorization: Bearer {JWT_TOKEN}
215 | Accept: application/vnd.jincor+json; version=1
216 |
217 | + Response 200 (application/json)
218 |
219 | {
220 | "status": 200
221 | }
222 |
223 | + Response 404 (application/json)
224 |
225 | {
226 | "status": 404,
227 | "error": "Not found"
228 | }
229 |
230 |
--------------------------------------------------------------------------------
/build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -ex
4 | IMAGE_NAME="jincort/backend-verify"
5 | TAG="${1}"
6 | docker build -t ${IMAGE_NAME}:${TAG} -f Dockerfile.prod .
7 | docker push ${IMAGE_NAME}:${TAG}
8 |
--------------------------------------------------------------------------------
/codeship-services.yml:
--------------------------------------------------------------------------------
1 | verify:
2 | build:
3 | image: registry.heroku.com/backend-verify/web
4 | context: ./
5 | dockerfile: Dockerfile.prod
6 | environment:
7 | REDIS_URL: 'redis://redis:6379'
8 | FORCE_HTTPS: disabled
9 | MAIL_DRIVER: dummy
10 |
11 | redis:
12 | image: registry.jincor.com/backend/redis:latest
13 | ports:
14 | - "6379"
15 |
16 |
17 | herokudeployment:
18 | image: codeship/heroku-deployment
19 | encrypted_env_file: deployment.env.encrypted
20 | volumes:
21 | - ./:/deploy
22 |
23 | dockercfg_generator:
24 | image: codeship/heroku-dockercfg-generator
25 | add_docker: true
26 | encrypted_env_file: deployment.env.encrypted
27 |
--------------------------------------------------------------------------------
/codeship-steps.yml:
--------------------------------------------------------------------------------
1 | - name: test
2 | service: verify
3 | command: yarn test
4 | - name: deploy
5 | service: verify
6 | tag: master
7 | type: push
8 | image_name: registry.heroku.com/backend-verify/web
9 | registry: registry.heroku.com
10 | dockercfg_service: dockercfg_generator
11 |
--------------------------------------------------------------------------------
/deployment.env.encrypted:
--------------------------------------------------------------------------------
1 | 54JVI0A5PJTMo7/g/hzIy75K8E8wO5MycxAlnfkGvrOVmkgZi0MthmNLVcfptb+i7oyfsLs8Sz5mWXZkeKoTs47avDU=
2 |
3 |
--------------------------------------------------------------------------------
/docker-compose.prod.yml:
--------------------------------------------------------------------------------
1 | version: '2'
2 | services:
3 |
4 | verify:
5 | image: registry.jincor.com/backend/verify:latest
6 | build:
7 | context: ./
8 | dockerfile: Dockerfile.prod
9 | environment:
10 | REDIS_URL: 'redis://redis:6379'
11 | FORCE_HTTPS: disabled
12 | AUTH_API_URL: ${AUTH_API_URL}
13 | MAIL_DRIVER: ${MAIL_DRIVER}
14 | MAILGUN_SECRET: ${MAILGUN_SECRET}
15 | MAILGUN_DOMAIN: ${MAILGUN_DOMAIN}
16 | ports:
17 | - "3000"
18 | - "4000"
19 | links:
20 | - redis
21 |
22 | redis:
23 | image: registry.jincor.com/backend/redis:latest
24 | ports:
25 | - "6379"
26 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '2'
2 | services:
3 |
4 | verify:
5 | image: registry.jincor.com/backend/verify-develop:latest
6 | build:
7 | context: ./
8 | dockerfile: Dockerfile
9 | environment:
10 | REDIS_URL: 'redis://redis:6379'
11 | FORCE_HTTPS: disabled
12 | MAIL_DRIVER: dummy
13 | tty: true
14 | ports:
15 | - "3000"
16 | - "4000"
17 | volumes:
18 | - ./:/usr/src/app
19 | links:
20 | - redis
21 |
22 | redis:
23 | image: jincort/backend-redis:production
24 | ports:
25 | - "6379"
26 |
--------------------------------------------------------------------------------
/docs/index.html:
--------------------------------------------------------------------------------
1 |
Jincor VERIFY Service
http://verify:3000/
Jincor Verification is a service for verify users email, phone, and etc.
2 | The main responsibilities are:
3 |
4 | Interact with a service provider
5 | Validation of a received code
6 |
7 |
8 |
JWT_TOKEN should be passed for every API call in the HTTP headers,
9 | that was received from auth service.
10 |
11 |
12 | /methods/{METHOD}/actions/initiate [POST]
13 |
14 |
15 | /methods/{METHOD}/verifiers/{VERIFICATION_ID}/actions/validate [POST]
16 |
17 |
18 | /methods/{METHOD}/verifiers/{VERIFICATION_ID} [DELETE]
19 |
20 |
21 |
22 |
23 |
24 | When you initiate google_auth verification 1st time for selected consumer you will get totpUri
to show QR code in your frontend app.
25 |
26 |
27 | The received secret will be TEMPORARY until you verify it first time. If secret is not verified at least 1 time and you initiate
new google_auth verification for consumer - you will get NEW secret. It’s required to ensure that user successfully stored the secret and entered correct code.
28 |
29 |
30 | When you want to disable 2FA - initiate verification and send removeSecret=true
param to validate
endpoint to remove consumer’s secret.
31 |
32 |
33 |
Verification: Initiate ¶ Initiate verification process, with usage of specified METHOD .
34 |
POST http://verify:3000/ /methods/METHOD /actions/initiate
Requests example 1
Headers Content-Type : application/json Authorization : Bearer {JWT_TOKEN} Accept : application/vnd.jincor+json; version=1
Body {
35 | "consumer ": "test@test.com" ,
36 | "issuer ": "Jincor" ,
37 | "template ": {
38 | "body ": "Click on the <a href=\"https://service/verify-email/5RkvAr0PUe708a?code={{{CODE}}}&verificationId={{{VERIFICATION_ID}}}\">Verify Link</a> to continue registration."
39 | } ,
40 | "generateCode ": {
41 | "length ": 32 ,
42 | "symbolSet ": [
43 | "DIGITS" ,
44 | "alphas" ,
45 | "ALPHAS"
46 | ]
47 | } ,
48 | "policy ": {
49 | "expiredOn ": "01:00:00"
50 | } ,
51 | "payload ": {
52 | "your ": "custom payload"
53 | }
54 | }
Responses 200 200 404 422
Headers Content-Type : application/json
Body {
55 | "status ": 200 ,
56 | "verificationId ": "dc910ae0-7c67-4ace-8ebb-9edd4b5d8b0f" ,
57 | "attempts ": 0 ,
58 | "expiredOn ": 1505817462 ,
59 | "payload ": {
60 | "your ": "custom payload"
61 | }
62 | }
Headers Content-Type : application/json
Body {
63 | "verificationId ": "5028c0cd-07a9-4fa7-8d88-49edd2a44b72" ,
64 | "consumer ": "test@test.com" ,
65 | "expiredOn ": 1508689744 ,
66 | "totpUri ": "otpauth://totp/:test@test.com?secret=CK53DOA3R7B2ZDMZKVOM53ZPT355ORJI&issuer=&algorithm=SHA1&digits=6&period=30" ,
67 | "status ": 200
68 | }
Headers Content-Type : application/json
Body {
69 | "status ": 404 ,
70 | "error ": "Method not supported"
71 | }
Headers Content-Type : application/json
Body {
72 | "status ": 422 ,
73 | "error ": "Invalid request" ,
74 | "details ": [
75 | {
76 | "path ": "generateCode.length" ,
77 | "error ": "Incorrect number format"
78 | }
79 | ]
80 | }
Initiate verification
POST /methods/{METHOD}/actions/initiate
Example: email verification /methods/email/actions/initiate
.
81 | Pass uuid in policy.forcedVerificationId
to force using of your verification generated id.
82 | Set up own code in policy.forcedCode
to force using of your verification code (does not apply for google_auth method).
83 |
METHOD string
(required) One of email , google_auth , phone (not implemented).
84 | POST http://verify:3000/ /methods/METHOD /verifiers/VERIFICATION_ID /actions/validate
Requests example 1
Headers Content-Type : application/json Authorization : Bearer {JWT_TOKEN} Accept : application/vnd.jincor+json; version=1
Body {
85 | "code ": "JeDknKO0EZRBT6aFPrFQhzcCA2aqyVsHzZeJ8Vf" ,
86 | "removeSecret ": true
87 | }
Responses 200 404 422
Headers Content-Type : application/json
Body {
88 | "status ": 200 ,
89 | "data ": {
90 | "verificationId ": "dc910ae0-7c67-4ace-8ebb-9edd4b5d8b0f" ,
91 | "consumer ": "test@test.com" ,
92 | "expiredOn ": 1505817462 ,
93 | "payload ": {
94 | "your ": "custom payload"
95 | } ,
96 | "attempts ": 0
97 | }
98 | }
Headers Content-Type : application/json
Body {
99 | "status ": 404 ,
100 | "error ": "Not found"
101 | }
Headers Content-Type : application/json
Body {
102 | "status ": 422 ,
103 | "error ": "Invalid code" ,
104 | "data ": {
105 | "verificationId ": "dc910ae0-7c67-4ace-8ebb-9edd4b5d8b0f" ,
106 | "consumer ": "test@test.com" ,
107 | "expiredOn ": 1505817462 ,
108 | "payload ": {
109 | "your ": "custom payload"
110 | } ,
111 | "attempts ": 1
112 | }
113 | }
Validate the code
POST /methods/{METHOD}/verifiers/{VERIFICATION_ID}/actions/validate
Example: code validation for the email
114 | method /methods/email/verifiers/dc910ae0-7c67-4ace-8ebb-9edd4b5d8b0f/actions/validate
.
115 |
123 |
METHOD string
(required) One of phone , email , google_auth .
124 | VERIFICATION_ID string
(required) GET http://verify:3000/ /methods/METHOD /verifiers/VERIFICATION_ID
Requests example 1
Headers Content-Type : application/json Authorization : Bearer {JWT_TOKEN} Accept : application/vnd.jincor+json; version=1
Responses 200 404
Headers Content-Type : application/json
Body {
125 | "status ": 200 ,
126 | "data ": {
127 | "verificationId ": "dc910ae0-7c67-4ace-8ebb-9edd4b5d8b0f" ,
128 | "consumer ": "test@test.com" ,
129 | "expiredOn ": 1505817462 ,
130 | "payload ": {
131 | "your ": "custom payload"
132 | } ,
133 | "attempts ": 1
134 | }
135 | }
Headers Content-Type : application/json
Body {
136 | "status ": 404 ,
137 | "error ": "Not found"
138 | }
Get verification
GET /methods/{METHOD}/verifiers/{VERIFICATION_ID}
METHOD string
(required) One of phone , email , google_auth .
139 | VERIFICATION_ID string
(required) DELETE http://verify:3000/ /methods/METHOD /verifiers/VERIFICATION_ID
Requests example 1
Headers Content-Type : application/json Authorization : Bearer {JWT_TOKEN} Accept : application/vnd.jincor+json; version=1
Responses 200 404
Headers Content-Type : application/json
Body {
140 | "status ": 200
141 | }
Headers Content-Type : application/json
Body {
142 | "status ": 404 ,
143 | "error ": "Not found"
144 | }
Invalidate the code
DELETE /methods/{METHOD}/verifiers/{VERIFICATION_ID}
METHOD string
(required) One of phone , email , google_auth .
145 | VERIFICATION_ID string
(required) Generated by aglio on 28 Nov 2017
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "verify",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "start": "nodemon -w ./src -e ts ./src/bin/www --exec ts-node",
7 | "lint": "tslint './src/**/*.ts'",
8 | "lintFix": "tslint --fix './src/**/*.ts'",
9 | "test": "nyc mocha ./src/**/*.spec.ts",
10 | "build": "tsc -p tsconfig.build.json --outDir dist",
11 | "serve": "node ./dist/bin/www.js"
12 | },
13 | "nyc": {
14 | "exclude": [
15 | "src/**/*.spec.ts"
16 | ]
17 | },
18 | "dependencies": {
19 | "@types/joi": "10.4.0",
20 | "authenticator": "1.1.3",
21 | "bcrypt-nodejs": "0.0.3",
22 | "body-parser": "~1.15.2",
23 | "debug": "~2.2.0",
24 | "express": "~4.14.0",
25 | "inversify": "4.2.0",
26 | "inversify-express-utils": "4.0.0",
27 | "joi": "10.6.0",
28 | "js-yaml": "^3.10.0",
29 | "jsonwebtoken": "^7.2.1",
30 | "lru-cache": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.1.tgz",
31 | "mailcomposer": "4.0.2",
32 | "mailgun-js": "0.13.1",
33 | "moment": "https://registry.npmjs.org/moment/-/moment-2.18.1.tgz",
34 | "node-mailjet": "^3.2.1",
35 | "node-uuid": "^1.4.7",
36 | "redis": "2.6.3",
37 | "reflect-metadata": "0.1.10",
38 | "request": "2.81.0",
39 | "rolling-rate-limiter": "0.1.9",
40 | "typemoq": "^2.0.1"
41 | },
42 | "devDependencies": {
43 | "@types/bcrypt-nodejs": "0.0.30",
44 | "@types/chai": "https://registry.npmjs.org/@types/chai/-/chai-3.4.34.tgz",
45 | "@types/chai-as-promised": "7.1.0",
46 | "@types/chai-http": "0.0.29",
47 | "@types/debug": "0.0.29",
48 | "@types/express": "4.0.34",
49 | "@types/jsonwebtoken": "7.2.0",
50 | "@types/mocha": "2.2.38",
51 | "@types/node": "7.0.0",
52 | "@types/node-uuid": "0.0.28",
53 | "@types/redis": "0.12.34",
54 | "@types/request": "2.0.3",
55 | "@types/sinon": "2.3.4",
56 | "chai": "https://registry.npmjs.org/chai/-/chai-3.5.0.tgz",
57 | "chai-as-promised": "7.1.1",
58 | "chai-http": "3.0.0",
59 | "mocha": "3.2.0",
60 | "nodemon": "^1.11.0",
61 | "npm-shrinkwrap": "^6.0.2",
62 | "nyc": "10.0.0",
63 | "sinon": "3.3.0",
64 | "ts-node": "2.0.0",
65 | "tslint": "5.7.0",
66 | "tslint-config-standard": "5.0.2",
67 | "typescript": "2.1.5"
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/bin/www.ts:
--------------------------------------------------------------------------------
1 | import server from '../server';
2 | import * as http from 'http';
3 | import * as https from 'https';
4 | import * as fs from 'fs';
5 | import config from '../config';
6 |
7 | /**
8 | * Create HTTP server.
9 | */
10 | const httpServer = http.createServer(server);
11 |
12 | /**
13 | * Listen on provided port, on all network interfaces.
14 | */
15 | httpServer.listen(config.server.port);
16 |
17 | if (config.server.httpsServer === 'enabled') {
18 | const httpsOptions = {
19 | key: fs.readFileSync(__dirname + '/server.key'),
20 | cert: fs.readFileSync(__dirname + '/auth.crt')
21 | };
22 | const httpsServer = https.createServer(httpsOptions, server);
23 | httpsServer.listen(config.server.httpsPort);
24 | }
25 |
--------------------------------------------------------------------------------
/src/config.ts:
--------------------------------------------------------------------------------
1 | const {
2 | REDIS_URL,
3 | PORT,
4 | HTTPS_PORT,
5 | HTTPS_SERVER,
6 | FORCE_HTTPS,
7 | AUTH_API_URL,
8 | AUTH_API_TIMEOUT
9 | } = process.env;
10 |
11 | export default {
12 | environment: {
13 | isTesting: process.env.LOADED_MOCHA_OPTS === 'true'
14 | },
15 | server: {
16 | port: parseInt(PORT, 10) || 3000,
17 | httpsPort: parseInt(HTTPS_PORT, 10) || 4000,
18 | httpsServer: HTTPS_SERVER || 'disabled',
19 | forceHttps: FORCE_HTTPS || 'disabled'
20 | },
21 | auth: {
22 | url: AUTH_API_URL || 'http://auth:3000/tenant/verify',
23 | timeout: parseInt(AUTH_API_TIMEOUT, 10) || 5000
24 | },
25 | redis: {
26 | url: REDIS_URL || 'redis://redis:6379',
27 | prefix: 'jincor_verify_'
28 | },
29 | providers: {
30 | email: {
31 | provider: process.env.MAIL_DRIVER || 'dummy'
32 | }
33 | }
34 | };
35 |
--------------------------------------------------------------------------------
/src/controllers/methods.ts:
--------------------------------------------------------------------------------
1 | import { Response } from 'express';
2 | import { inject, injectable } from 'inversify';
3 | import { controller, httpPost } from 'inversify-express-utils';
4 | import 'reflect-metadata';
5 | import { InvalidParametersException } from '../exceptions/exceptions';
6 | import { responseWithError, responseAsUnbehaviorError } from '../helpers/responses';
7 | import { AuthorizedRequest } from '../middlewares/common';
8 |
9 | import {
10 | VerificationServiceFactory,
11 | VerificationServiceFactoryType
12 | } from '../services/verifications';
13 |
14 | interface MethodRequest extends AuthorizedRequest {
15 | params: {
16 | method: string;
17 | };
18 | }
19 |
20 | /**
21 | * MethodsController resource
22 | */
23 | @injectable()
24 | @controller(
25 | '/methods',
26 | 'AuthMiddleware'
27 | )
28 | export class MethodsController {
29 | constructor( @inject(VerificationServiceFactoryType) private verificationFactory: VerificationServiceFactory) {
30 | }
31 |
32 | /**
33 | * Initiate verification process for specified method.
34 | *
35 | * @param req express req object
36 | * @param res express res object
37 | */
38 | @httpPost(
39 | '/:method/actions/initiate',
40 | 'SupportedMethodsMiddleware',
41 | 'InitiateVerificationValidation'
42 | )
43 | async initiate(req: MethodRequest, res: Response): Promise {
44 | try {
45 | const verificationService = this.verificationFactory.create(req.params.method);
46 | const verificationDetails = await verificationService.initiate(req.body, req.tenant);
47 | res.json(Object.assign({}, verificationDetails, { status: 200 }));
48 | } catch (err) {
49 | if (err instanceof InvalidParametersException) {
50 | responseWithError(res, 422, {
51 | 'error': err.name,
52 | 'details': err.details
53 | });
54 | } else {
55 | responseAsUnbehaviorError(res, err);
56 | }
57 | }
58 | }
59 |
60 | }
61 |
--------------------------------------------------------------------------------
/src/controllers/specs/methods.spec.ts:
--------------------------------------------------------------------------------
1 | import * as chai from 'chai';
2 | import app from '../../server';
3 |
4 | chai.use(require('chai-http'));
5 | const {expect, request} = chai;
6 |
7 | function createRequest(path: string, data: any) {
8 | return request(app).post(path)
9 | .set('Accept', 'application/json')
10 | .set('Authorization', 'Bearer TOKEN').send(data);
11 | }
12 |
13 | describe('Test Methods controller', () => {
14 |
15 | it('will initiate a email verification process', (done) => {
16 | const forcedId = '395a0e7d-3a1f-4d51-8dad-7d0229bd64ac';
17 | createRequest('/methods/email/actions/initiate', {
18 | consumer: 'test@test.com',
19 | template: {
20 | fromName: 'Sender Name',
21 | fromEmail: 'source@email.com',
22 | body: 'body'
23 | },
24 | policy: {
25 | expiredOn: '00:01:00',
26 | forcedCode: '123456',
27 | forcedVerificationId: forcedId
28 | }
29 | }).end((err, res) => {
30 | expect(res.status).is.equals(200);
31 | expect(res.body.verificationId).is.equals(forcedId);
32 | expect(res.body.expiredOn).is.greaterThan(~~((+new Date()) / 1000));
33 | done();
34 | });
35 | });
36 |
37 | it('will initiate a email verification process and accept payload', (done) => {
38 | const forcedId = '395a0e7d-3a1f-4d51-8dad-7d0229bd64ac';
39 | createRequest('/methods/email/actions/initiate', {
40 | consumer: 'test@test.com',
41 | template: {
42 | fromName: 'Sender Name',
43 | fromEmail: 'source@email.com',
44 | body: 'body'
45 | },
46 | policy: {
47 | expiredOn: '00:01:00',
48 | forcedCode: '123456',
49 | forcedVerificationId: forcedId
50 | },
51 | payload: {
52 | sub: {
53 | key: 'value'
54 | },
55 | key: 'value'
56 | }
57 | }).end((err, res) => {
58 | expect(res.status).is.equals(200);
59 | expect(res.body.verificationId).is.equals(forcedId);
60 | expect(res.body.expiredOn).is.greaterThan(~~((+new Date()) / 1000));
61 | expect(res.body.payload).to.deep.eq({
62 | sub: {
63 | key: 'value'
64 | },
65 | key: 'value'
66 | });
67 | done();
68 | });
69 | });
70 |
71 | it('will initiate an authenticator verification process', (done) => {
72 | const params = {
73 | consumer: 'test@test.com',
74 | issuer: 'Jincor',
75 | policy: {
76 | expiredOn: '00:01:00'
77 | },
78 | payload: {
79 | sub: {
80 | key: 'value'
81 | },
82 | key: 'value'
83 | }
84 | };
85 |
86 | createRequest('/methods/google_auth/actions/initiate', params).end((err, res) => {
87 | expect(res.status).is.equals(200);
88 | expect(res.body).to.have.property('verificationId');
89 | expect(res.body.expiredOn).is.greaterThan(~~((+new Date()) / 1000));
90 | expect(res.body.totpUri).contains('otpauth://totp/Jincor:test@test.com?secret=');
91 | expect(res.body.payload).to.deep.eq({
92 | sub: {
93 | key: 'value'
94 | },
95 | key: 'value'
96 | });
97 | done();
98 | });
99 | });
100 |
101 | it('will initiate an authenticator verification process and accept payload', (done) => {
102 | const params = {
103 | consumer: 'test@test.com',
104 | issuer: 'Jincor',
105 | policy: {
106 | expiredOn: '00:01:00'
107 | }
108 | };
109 |
110 | createRequest('/methods/google_auth/actions/initiate', params).end((err, res) => {
111 | expect(res.status).is.equals(200);
112 | expect(res.body).to.have.property('verificationId');
113 | expect(res.body.expiredOn).is.greaterThan(~~((+new Date()) / 1000));
114 | expect(res.body.totpUri).contains('otpauth://totp/Jincor:test@test.com?secret=');
115 | done();
116 | });
117 | });
118 |
119 | it('will require issuer param for google_auth initiate', (done) => {
120 | const params = {
121 | consumer: 'test@test.com',
122 | policy: {
123 | expiredOn: '00:01:00'
124 | }
125 | };
126 |
127 | createRequest('/methods/google_auth/actions/initiate', params).end((err, res) => {
128 | expect(res.status).is.equals(422);
129 | done();
130 | });
131 | });
132 |
133 | it('will fail of initiation of email verification process if no template body presents', (done) => {
134 | const forcedId = '395a0e7d-3a1f-4d51-8dad-7d0229bd64ac';
135 | createRequest('/methods/email/actions/initiate', {
136 | consumer: 'test@test.com',
137 | template: {
138 | },
139 | policy: {
140 | expiredOn: '00:01:00',
141 | forcedCode: '123456',
142 | forcedVerificationId: forcedId
143 | }
144 | }).end((err, res) => {
145 | expect(res.status).is.equals(422);
146 | expect(res.body.error).is.equals('Invalid request');
147 | done();
148 | });
149 | });
150 |
151 | });
152 |
--------------------------------------------------------------------------------
/src/controllers/specs/verifiers.spec.ts:
--------------------------------------------------------------------------------
1 | import * as chai from 'chai';
2 | import app from '../../server';
3 | import * as express from 'express';
4 | import * as TypeMoq from 'typemoq';
5 | import { container } from '../../ioc.container';
6 | import { InversifyExpressServer } from 'inversify-express-utils';
7 | import * as bodyParser from 'body-parser';
8 | import { VerificationServiceFactoryType, VerificationServiceFactory, VerificationServiceFactoryRegister } from '../../services/verifications';
9 | import AuthenticatorVerificationService from '../../services/authenticator.verification';
10 | import { SimpleInMemoryStorageService } from '../../services/storages';
11 | import * as authenticator from 'authenticator';
12 |
13 | chai.use(require('chai-http'));
14 | const { expect, request } = chai;
15 |
16 | function createRequest(path: string, data: any, method: string = 'post', customApp?: any) {
17 | const appToUse = customApp || app;
18 |
19 | return request(appToUse)[method](path)
20 | .set('Accept', 'application/json')
21 | .set('Authorization', 'Bearer TOKEN').send(data);
22 | }
23 |
24 | function createInitiateEmailVerification(verificationId: string, code: string) {
25 | return createRequest('/methods/email/actions/initiate', {
26 | consumer: 'test@test.com',
27 | template: {
28 | body: 'body'
29 | },
30 | policy: {
31 | expiredOn: '00:01:00',
32 | forcedCode: code,
33 | forcedVerificationId: verificationId
34 | },
35 | payload: {
36 | key: 'value'
37 | }
38 | });
39 | }
40 |
41 | function mockStorageForGoogleAuth(secret, verificationId, verificationData) {
42 | const customApp = express();
43 |
44 | const storageMock = TypeMoq.Mock.ofType(SimpleInMemoryStorageService);
45 | storageMock.setup(x => x.get(TypeMoq.It.isValue(`test${ verificationId }`), TypeMoq.It.isAny()))
46 | .returns(async(): Promise => verificationData);
47 |
48 | storageMock.setup(x => x.get(TypeMoq.It.isValue('testtenantIdtest@test.com'), TypeMoq.It.isAny()))
49 | .returns(async(): Promise => secret);
50 |
51 | const authenticatorService = new AuthenticatorVerificationService('test', storageMock.object);
52 |
53 | const factoryMock = TypeMoq.Mock.ofType(VerificationServiceFactoryRegister);
54 |
55 | factoryMock.setup(x => x.create(TypeMoq.It.isAny()))
56 | .returns(() => authenticatorService);
57 |
58 | container.rebind(VerificationServiceFactoryType).toConstantValue(factoryMock.object);
59 |
60 | customApp.use(bodyParser.json());
61 | customApp.use(bodyParser.urlencoded({ extended: false }));
62 |
63 | const server = new InversifyExpressServer(container, null, null, customApp);
64 | return server.build();
65 | }
66 |
67 | describe('Test Verifier controller', () => {
68 |
69 | const notExistsVerificationId = 'afde0e7d-3a1f-4d51-8dad-7d0229bd64c4';
70 | const forcedId = '395a0e7d-3a1f-4d51-8dad-7d0229bd64ac';
71 | const forcedCode = '12345678';
72 |
73 | it('will successfully validate verificationId and code - email', (done) => {
74 |
75 | createInitiateEmailVerification(forcedId, forcedCode).end((err, res) => {
76 | expect(res.status).is.equals(200);
77 | expect(res.body.verificationId).is.equals(forcedId);
78 |
79 | createRequest(`/methods/email/verifiers/${forcedId}/actions/validate`, {
80 | code: forcedCode
81 | }).end((err, res) => {
82 | expect(res.status).is.equals(200);
83 | expect(res.body.data.verificationId).is.equals('395a0e7d-3a1f-4d51-8dad-7d0229bd64ac');
84 | expect(res.body.data.consumer).is.equals('test@test.com');
85 | expect(res.body.data.expiredOn).to.be.an('number');
86 | expect(res.body.data.payload).to.deep.eq({
87 | key: 'value'
88 | });
89 | done();
90 | });
91 | });
92 |
93 | });
94 |
95 | it('will respond with 422 for incorrect code and increase attempts count - email', (done) => {
96 |
97 | createInitiateEmailVerification(forcedId, forcedCode).end((err, res) => {
98 | expect(res.status).is.equals(200);
99 | expect(res.body.verificationId).is.equals(forcedId);
100 |
101 | createRequest(`/methods/email/verifiers/${forcedId}/actions/validate`, {
102 | code: '123456'
103 | }).end((err, res) => {
104 | expect(res.status).is.equals(422);
105 | expect(res.body.data.verificationId).is.equals('395a0e7d-3a1f-4d51-8dad-7d0229bd64ac');
106 | expect(res.body.data.consumer).is.equals('test@test.com');
107 | expect(res.body.data.expiredOn).to.be.an('number');
108 | expect(res.body.data.payload).to.deep.eq({
109 | key: 'value'
110 | });
111 | expect(res.body.data.attempts).to.eq(1);
112 | expect(res.body.data).to.not.have.property('code');
113 | done();
114 | });
115 | });
116 |
117 | });
118 |
119 | it('will successfully get verification data - email', (done) => {
120 |
121 | createInitiateEmailVerification(forcedId, forcedCode).end((err, res) => {
122 | expect(res.status).is.equals(200);
123 | expect(res.body.verificationId).is.equals(forcedId);
124 |
125 | createRequest(`/methods/email/verifiers/${forcedId}`, null, 'get').end((err, res) => {
126 | expect(res.status).is.equals(200);
127 | expect(res.body.data.verificationId).is.equals('395a0e7d-3a1f-4d51-8dad-7d0229bd64ac');
128 | expect(res.body.data.consumer).is.equals('test@test.com');
129 | expect(res.body.data.expiredOn).to.be.an('number');
130 | expect(res.body.data.payload).to.deep.eq({
131 | key: 'value'
132 | });
133 | expect(res.body.data.attempts).to.eq(0);
134 | expect(res.body.data).to.not.have.property('code');
135 | done();
136 | });
137 | });
138 |
139 | });
140 |
141 | it('should respond with 404 if verification is not found - email', (done) => {
142 | createRequest(`/methods/email/verifiers/randomId`, null, 'get').end((err, res) => {
143 | expect(res.status).is.equals(404);
144 | done();
145 | });
146 | });
147 |
148 | it('will fail validation if verificationId not exists', (done) => {
149 |
150 | createRequest(`/methods/email/verifiers/${notExistsVerificationId}/actions/validate`, {
151 | code: '123456'
152 | }).end((err, res) => {
153 | expect(res.status).is.equals(404);
154 | expect(res.body.error).is.equals('Not found');
155 | done();
156 | });
157 |
158 | });
159 |
160 | it('will successfully remove verificationId', (done) => {
161 |
162 | createInitiateEmailVerification(forcedId, forcedCode).end((err, res) => {
163 | expect(res.status).is.equals(200);
164 | expect(res.body.verificationId).is.equals(forcedId);
165 |
166 | createRequest(`/methods/email/verifiers/${forcedId}`, {}, 'delete').end((err, res) => {
167 | expect(res.status).is.equals(200);
168 | done();
169 | });
170 | });
171 |
172 | });
173 |
174 | it('will fail remove if verificationId doesn\'t exists', (done) => {
175 |
176 | createRequest(`/methods/email/verifiers/${notExistsVerificationId}`, {}, 'delete').end((err, res) => {
177 | expect(res.status).is.equals(404);
178 | expect(res.body.error).is.equals('Not found');
179 | done();
180 | });
181 |
182 | });
183 |
184 | it('will successfully validate verificationId and code - authenticator', (done) => {
185 | const secret = {
186 | secret: '3qlq j5uj gdcj xoqt 6rhu yglx 5mf5 i7ll',
187 | verified: true
188 | };
189 | const verificationId = 'verificationId';
190 | const verificationData = {
191 | consumer: 'test@test.com',
192 | payload: {
193 | key: 'value'
194 | },
195 | attempts: 0
196 | };
197 |
198 | const testApp = mockStorageForGoogleAuth(secret, verificationId, verificationData);
199 | createRequest(`/methods/google_auth/verifiers/${ verificationId }/actions/validate`, {
200 | code: authenticator.generateToken(secret.secret)
201 | }, 'post', testApp).end((err, res) => {
202 | expect(res.status).is.equals(200);
203 | expect(res.body.data.consumer).is.eq('test@test.com');
204 | expect(res.body.data.payload).to.deep.eq({
205 | key: 'value'
206 | });
207 | done();
208 | });
209 | });
210 |
211 | it('will respond with 422 for incorrect code and increase attempts count - authenticator', (done) => {
212 | const secret = {
213 | secret: '3qlq j5uj gdcj xoqt 6rhu yglx 5mf5 i7ll',
214 | verified: true
215 | };
216 | const verificationId = 'verificationId';
217 | const verificationData = {
218 | consumer: 'test@test.com',
219 | payload: {
220 | key: 'value'
221 | },
222 | attempts: 0
223 | };
224 |
225 | const testApp = mockStorageForGoogleAuth(secret, verificationId, verificationData);
226 | createRequest(`/methods/google_auth/verifiers/${ verificationId }/actions/validate`, {
227 | code: authenticator.generateToken(secret.secret) + '1'
228 | }, 'post', testApp).end((err, res) => {
229 | expect(res.status).is.equals(422);
230 | expect(res.body.data.consumer).is.eq('test@test.com');
231 | expect(res.body.data.payload).to.deep.eq({
232 | key: 'value'
233 | });
234 | expect(res.body.data.attempts).to.eq(1);
235 | expect(res.body.data).to.not.have.property('code');
236 | done();
237 | });
238 | });
239 | });
240 |
--------------------------------------------------------------------------------
/src/controllers/verifiers.ts:
--------------------------------------------------------------------------------
1 | import { Response } from 'express';
2 | import { inject, injectable } from 'inversify';
3 | import { controller, httpDelete, httpPost, httpGet } from 'inversify-express-utils';
4 | import 'reflect-metadata';
5 | import { NotFoundException } from '../exceptions/exceptions';
6 | import { responseWithError, responseAsUnbehaviorError } from '../helpers/responses';
7 | import { AuthorizedRequest } from '../middlewares/common';
8 |
9 | import {
10 | VerificationServiceFactory,
11 | VerificationServiceFactoryType
12 | } from '../services/verifications';
13 |
14 | interface VerifierRequest extends AuthorizedRequest {
15 | params: {
16 | method: string;
17 | verificationId: string;
18 | };
19 | }
20 |
21 | /**
22 | * VerifiersController resource
23 | */
24 | @injectable()
25 | @controller(
26 | '/methods/:method/verifiers',
27 | 'AuthMiddleware',
28 | 'SupportedMethodsMiddleware'
29 | )
30 | export class VerifiersController {
31 |
32 | constructor( @inject(VerificationServiceFactoryType) private verificationFactory: VerificationServiceFactory) {
33 | }
34 |
35 | /**
36 | * Verify code for specified Verification Id.
37 | *
38 | * @param req express req object
39 | * @param res express res object
40 | */
41 | @httpPost(
42 | '/:verificationId/actions/validate'
43 | )
44 | async validate(req: VerifierRequest, res: Response): Promise {
45 | try {
46 | const verificationService = this.verificationFactory.create(req.params.method);
47 | const validationResult = await verificationService.validate(req.params.verificationId, req.body, req.tenant);
48 | if (!validationResult.isValid) {
49 | responseWithError(res, 422, {
50 | error: 'Invalid code',
51 | data: validationResult.verification
52 | });
53 | } else {
54 | this.responseSuccessfully(res, validationResult.verification);
55 | }
56 | } catch (err) {
57 | if (err instanceof NotFoundException) {
58 | this.responseAsNotFound(res);
59 | } else {
60 | responseAsUnbehaviorError(res, err);
61 | }
62 | }
63 | }
64 |
65 | /**
66 | * Delete Verification Id.
67 | *
68 | * @param req express req object
69 | * @param res express res object
70 | */
71 | @httpDelete(
72 | '/:verificationId'
73 | )
74 | async remove(req: VerifierRequest, res: Response): Promise {
75 | try {
76 | const verificationService = this.verificationFactory.create(req.params.method);
77 | if (!await verificationService.remove(req.params.verificationId)) {
78 | throw new NotFoundException();
79 | }
80 | this.responseSuccessfully(res);
81 | } catch (err) {
82 | if (err instanceof NotFoundException) {
83 | this.responseAsNotFound(res);
84 | } else {
85 | responseAsUnbehaviorError(res, err);
86 | }
87 | }
88 | }
89 |
90 | /**
91 | * Verify code for specified Verification Id.
92 | *
93 | * @param req express req object
94 | * @param res express res object
95 | */
96 | @httpGet(
97 | '/:verificationId'
98 | )
99 | async getVerification(req: VerifierRequest, res: Response): Promise {
100 | const verificationService = this.verificationFactory.create(req.params.method);
101 | const verification = await verificationService.getVerification(req.params.verificationId);
102 | if (verification) {
103 | delete verification.code;
104 | this.responseSuccessfully(res, verification);
105 | } else {
106 | this.responseAsNotFound(res);
107 | }
108 | }
109 |
110 | private responseSuccessfully(res: Response, data?: any) {
111 | res.json({
112 | status: 200,
113 | data
114 | });
115 | }
116 |
117 | private responseAsNotFound(res: Response) {
118 | responseWithError(res, 404, {
119 | 'error': 'Not found'
120 | });
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/src/exceptions/exceptions.ts:
--------------------------------------------------------------------------------
1 | export class InvalidParametersException extends Error {
2 | constructor(public details: any) {
3 | super(details);
4 | this.name = 'Invalid request';
5 | }
6 | }
7 |
8 | export class NotFoundException extends Error {
9 |
10 | }
11 |
--------------------------------------------------------------------------------
/src/helpers/responses.ts:
--------------------------------------------------------------------------------
1 | import { Response } from 'express';
2 |
3 | /**
4 | * Format default error response
5 | * @param res
6 | * @param status
7 | * @param responseJson
8 | */
9 | export function responseWithError(res: Response, status: number, responseJson: Object) {
10 | return res.status(status).json(Object.assign({}, responseJson, { status: status }));
11 | }
12 |
13 | /**
14 | * Format response for 500 error
15 | * @param res
16 | * @param err
17 | */
18 | export function responseAsUnbehaviorError(res: Response, err: Error) {
19 | return responseWithError(res, 500, {
20 | 'error': err && err.name || err,
21 | 'message': err && err.message || ''
22 | });
23 | }
24 |
--------------------------------------------------------------------------------
/src/helpers/specs/responses.spec.ts:
--------------------------------------------------------------------------------
1 | import * as chai from 'chai';
2 | import * as sinon from 'sinon';
3 | import { responseWithError, responseAsUnbehaviorError } from '../responses';
4 |
5 | const {expect} = chai;
6 |
7 | describe('Helpers Responses', () => {
8 |
9 | let stubResponse;
10 | let mock;
11 | beforeEach(() => {
12 | stubResponse = {
13 | status: () => {
14 | return 200;
15 | },
16 | json: () => {
17 | return {};
18 | }
19 | };
20 | mock = sinon.mock(stubResponse);
21 | mock.expects('status').once().returns(stubResponse);
22 | mock.expects('json').once().returnsArg(0);
23 | });
24 |
25 | it('will provide a status code in the result json by responseWithError', () => {
26 | expect(responseWithError(stubResponse as any, 404, { 'error': 'Not found' }))
27 | .is.deep.equals({ 'error': 'Not found', 'status': 404 });
28 |
29 | mock.verify();
30 | });
31 |
32 | it('will provide a status code and details in the result json by responseAsUnbehaviorError', () => {
33 | expect(responseAsUnbehaviorError(stubResponse as any, new Error('Message')))
34 | .is.deep.equals({ 'error': 'Error', 'message': 'Message', 'status' : 500 });
35 |
36 | mock.verify();
37 | });
38 |
39 | });
40 |
--------------------------------------------------------------------------------
/src/index.d.ts:
--------------------------------------------------------------------------------
1 | declare interface VerificationData {
2 | id: string;
3 | consumer: string;
4 | attempts: number;
5 | expiredOn: number;
6 | code?: string;
7 | }
8 |
9 | declare interface ValidationResult {
10 | isValid: boolean;
11 | verification?: VerificationData;
12 | }
13 |
14 | /**
15 | * Storage Options
16 | */
17 | declare interface ValueOptions {
18 | ttlInSeconds: number;
19 | }
20 |
21 | /**
22 | * StorageService interface.
23 | */
24 | declare interface StorageService {
25 |
26 | /**
27 | * Set value with expiration options
28 | *
29 | * @param name
30 | * @param value
31 | * @param options
32 | */
33 | set(name: string, value: T, options?: ValueOptions): Promise;
34 |
35 | /**
36 | * Remove value
37 | *
38 | * @param name
39 | */
40 | remove(name: string): Promise;
41 |
42 | /**
43 | * Get value
44 | *
45 | * @param name
46 | * @param defaultValue
47 | */
48 | get(name: string, defaultValue: T): Promise;
49 |
50 | }
51 |
52 | /**
53 | * VerificationService interface.
54 | */
55 | declare interface VerificationService {
56 | initiate(params: any, tenantData: TenantVerificationResult): Promise;
57 | validate(verificationId: string, params: any, tenantData: TenantVerificationResult): Promise;
58 | remove(verificationId: string): Promise;
59 | getVerification(verificationId: string): Promise;
60 | }
61 |
62 | declare interface ParamsType {
63 | consumer: string;
64 | template?: any;
65 | generateCode?: GenerateCodeType;
66 | payload?: any;
67 | policy: PolicyParamsType;
68 | }
69 |
70 | declare interface PolicyParamsType {
71 | forcedVerificationId?: string;
72 | forcedCode?: string;
73 | expiredOn: string;
74 | }
75 |
76 | declare interface GenerateCodeType {
77 | symbolSet: Array;
78 | length: number;
79 | }
80 |
81 | declare interface EmailTemplateType {
82 | fromEmail: string;
83 | fromName?: string;
84 | subject: string;
85 | body: string;
86 | }
87 |
88 | declare interface TenantVerificationResult {
89 | id: string;
90 | login: string;
91 | jti: string;
92 | iat: number;
93 | aud: string;
94 | isTenant: boolean;
95 | }
96 |
97 | declare interface AuthenticatorSecret {
98 | secret: string;
99 | verified: boolean;
100 | }
101 |
--------------------------------------------------------------------------------
/src/ioc.container.ts:
--------------------------------------------------------------------------------
1 | import { interfaces as InversifyInterfaces, Container } from 'inversify';
2 | import { interfaces, TYPE } from 'inversify-express-utils';
3 | import * as express from 'express';
4 |
5 | import { MethodsController } from './controllers/methods';
6 | import { VerifiersController } from './controllers/verifiers';
7 |
8 | import config from './config';
9 |
10 | import * as commonMiddlewares from './middlewares/common';
11 | import * as validationMiddlewares from './middlewares/requests';
12 | import * as auths from './services/auth';
13 | import * as storages from './services/storages';
14 | import * as providers from './services/providers';
15 | import * as verifications from './services/verifications';
16 |
17 | let container = new Container();
18 |
19 | // services
20 | /* istanbul ignore else */
21 | if (config.environment.isTesting) {
22 | container.bind(auths.AuthenticationServiceType)
23 | .to(auths.JwtSingleInlineAuthenticationService);
24 | } else {
25 | container.bind(auths.AuthenticationServiceType)
26 | .toDynamicValue((context: InversifyInterfaces.Context): auths.AuthenticationService => {
27 | return new auths.CachedAuthenticationDecorator(
28 | context.container.resolve(auths.ExternalHttpJwtAuthenticationService)
29 | );
30 | }).inSingletonScope();
31 | }
32 |
33 | container.bind(providers.EmailProviderServiceType)
34 | .to(providers.EmailProviderService).inSingletonScope();
35 |
36 | /* istanbul ignore else */
37 | if (config.environment.isTesting) {
38 | container.bind(storages.StorageServiceType)
39 | .to(storages.SimpleInMemoryStorageService).inSingletonScope();
40 | } else {
41 | container.bind(storages.StorageServiceType)
42 | .to(storages.RedisStorageService).inSingletonScope();
43 | }
44 |
45 | container.bind(verifications.VerificationServiceFactoryType)
46 | .to(verifications.VerificationServiceFactoryRegister).inSingletonScope();
47 |
48 | // middlewares
49 | container.bind(commonMiddlewares.AuthMiddlewareType)
50 | .to(commonMiddlewares.AuthMiddleware);
51 |
52 | container.bind(commonMiddlewares.SupportedMethodsMiddlewareType)
53 | .to(commonMiddlewares.SupportedMethodsMiddleware);
54 |
55 | const authMiddleware = container
56 | .get(commonMiddlewares.AuthMiddlewareType);
57 |
58 | /* istanbul ignore next */
59 | container.bind('AuthMiddleware').toConstantValue(
60 | (req: any, res: any, next: any) => authMiddleware.execute(req, res, next)
61 | );
62 |
63 | const supportedMethodsMiddleware = container
64 | .get(commonMiddlewares.SupportedMethodsMiddlewareType);
65 |
66 | /* istanbul ignore next */
67 | container.bind('SupportedMethodsMiddleware').toConstantValue(
68 | (req: any, res: any, next: any) => supportedMethodsMiddleware.execute(req, res, next)
69 | );
70 |
71 | container.bind('InitiateVerificationValidation').toConstantValue(
72 | (req: any, res: any, next: any) => validationMiddlewares.initiateVerification(req, res, next)
73 | );
74 |
75 | // controllers
76 | container.bind(TYPE.Controller).to(MethodsController).whenTargetNamed('MethodsController');
77 | container.bind(TYPE.Controller).to(VerifiersController).whenTargetNamed('VerifiersController');
78 |
79 | export { container };
80 |
--------------------------------------------------------------------------------
/src/middlewares/common.ts:
--------------------------------------------------------------------------------
1 | import { Response, Request, NextFunction } from 'express';
2 | import { inject, injectable } from 'inversify';
3 |
4 | import { VerificationServiceFactory, VerificationServiceFactoryType } from '../services/verifications';
5 | import { AuthenticationService, AuthenticationServiceType, AuthenticationException } from '../services/auth';
6 | import { responseWithError } from '../helpers/responses';
7 | export const AuthMiddlewareType = Symbol('AuthMiddlewareType');
8 | export const SupportedMethodsMiddlewareType = Symbol('SupportedMethodsMiddlewareType');
9 |
10 | export interface AuthorizedRequest extends Request {
11 | tenant?: TenantVerificationResult;
12 | }
13 |
14 | /**
15 | * Authentication middleware.
16 | */
17 | @injectable()
18 | export class AuthMiddleware {
19 | constructor( @inject(AuthenticationServiceType) private authenticationService: AuthenticationService) {
20 | }
21 |
22 | /**
23 | * Execute authentication
24 | *
25 | * @param req Request
26 | * @param res Response
27 | * @param next NextFunction
28 | */
29 | async execute(req: AuthorizedRequest, res: Response, next: NextFunction) {
30 | try {
31 | const jwtToken = this.extractJwtFromRequestHeaders(req);
32 | const tenantData = await this.authenticationService.validate(jwtToken);
33 | if (!jwtToken || !tenantData) {
34 | return responseWithError(res, 401, { error: 'Not Authorized' });
35 | }
36 |
37 | req.tenant = tenantData;
38 | return next();
39 | } catch (error) {
40 | if (error instanceof AuthenticationException) {
41 | return responseWithError(res, 500, { error: error.message });
42 | }
43 | return responseWithError(res, 500, { error });
44 | }
45 | }
46 |
47 | private extractJwtFromRequestHeaders(req: Request): string | null {
48 | if (!req.headers.authorization) {
49 | return null;
50 | }
51 |
52 | const parts = req.headers.authorization.split(' ');
53 |
54 | if (parts[0] !== 'Bearer' || !parts[1]) {
55 | return null;
56 | }
57 |
58 | return parts[1];
59 | }
60 | }
61 |
62 | /**
63 | * Filter only supported methods.
64 | */
65 | @injectable()
66 | export class SupportedMethodsMiddleware {
67 | constructor( @inject(VerificationServiceFactoryType) private verificationFactory: VerificationServiceFactory) {
68 | }
69 |
70 | execute(req: Request, res: Response, next: NextFunction) {
71 | if (!this.verificationFactory.hasMethod(req.params.method)) {
72 | return responseWithError(res, 404, { error: 'Method not supported' });
73 | }
74 |
75 | return next();
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/src/middlewares/requests.ts:
--------------------------------------------------------------------------------
1 | import { responseWithError } from '../helpers/responses';
2 | import * as Joi from 'joi';
3 | import { Response, Request, NextFunction } from 'express';
4 |
5 | const options = {
6 | allowUnknown: true
7 | };
8 |
9 | const jsonSchemeValidateRequest = Joi.object().keys({
10 | code: Joi.string().min(1).required()
11 | });
12 |
13 | function commonFlowRequestMiddleware(scheme: Joi.Schema, req: Request, res: Response, next: NextFunction) {
14 | const result = Joi.validate(req.body || {}, scheme, options);
15 |
16 | if (result.error) {
17 | return responseWithError(res, 422, {
18 | 'error': result.error,
19 | 'details': result.value
20 | });
21 | } else {
22 | return next();
23 | }
24 | }
25 |
26 | export function validateRequest(req: Request, res: Response, next: NextFunction) {
27 | return commonFlowRequestMiddleware(jsonSchemeValidateRequest, req, res, next);
28 | }
29 |
30 | export function initiateVerification(req: Request, res: Response, next: NextFunction) {
31 | let scheme;
32 |
33 | if (req.params.method === 'email') {
34 | scheme = Joi.object().keys({
35 | consumer: Joi.string().required().empty(),
36 |
37 | template: Joi.object({}),
38 |
39 | generateCode: Joi.when('policy.forcedCode', {
40 | is: null,
41 | then: Joi.object({
42 | length: Joi.number().greater(0).less(32).required(),
43 | symbolSet: Joi.array().items(Joi.string().empty()).required()
44 | })
45 | }),
46 |
47 | policy: Joi.object({
48 | expiredOn: Joi.string().required().empty(),
49 | forcedVerificationId: Joi.string().empty().guid(),
50 | forcedCode: Joi.string().empty()
51 | }).required()
52 | });
53 | }
54 |
55 | if (req.params.method === 'google_auth') {
56 | scheme = Joi.object().keys({
57 | consumer: Joi.string().required().empty(),
58 | issuer: Joi.string().required(),
59 |
60 | policy: Joi.object({
61 | expiredOn: Joi.string().required().empty(),
62 | forcedVerificationId: Joi.string().empty().guid()
63 | }).required()
64 | });
65 | }
66 |
67 | return commonFlowRequestMiddleware(scheme, req, res, next);
68 | }
69 |
--------------------------------------------------------------------------------
/src/middlewares/specs/common.spec.ts:
--------------------------------------------------------------------------------
1 | import * as express from 'express';
2 | import * as chai from 'chai';
3 | import { container } from '../../ioc.container';
4 | import { AuthMiddleware, AuthMiddlewareType, SupportedMethodsMiddleware } from '../common';
5 | import { AuthenticationService, AuthenticationServiceType, JwtSingleInlineAuthenticationService } from '../../services/auth';
6 |
7 | chai.use(require('chai-http'));
8 | const {expect, request} = chai;
9 |
10 | describe('Common Middlewares', () => {
11 |
12 | describe('Test AuthMiddleware', () => {
13 |
14 | const authMiddleware = container.resolve(AuthMiddleware);
15 | const app: express.Application = express();
16 | app.use((req: express.Request, res: express.Response, next: express.NextFunction) => authMiddleware.execute(req, res, next));
17 |
18 | it('will not authorize without JWT token in the header', (done) => {
19 | request(app).get('/any-url').end((err, res) => {
20 | expect(res.status).to.equal(401);
21 | done();
22 | });
23 | });
24 |
25 | it('will not authorize with JWT token that have invalid formatted', (done) => {
26 | request(app).get('/any-url').set('Authorization', 'InvalidHeaderValue').end((err, res) => {
27 | expect(res.status).to.equal(401);
28 | done();
29 | });
30 | });
31 |
32 | it('will authorize with valid JWT token in the header', (done) => {
33 | request(app).get('/any-url').set('Authorization', 'Bearer TOKEN').end((err, res) => {
34 | expect(res.status).to.equal(404);
35 | done();
36 | });
37 | });
38 |
39 | });
40 |
41 | describe('Test SupportedMethodsMiddleware', () => {
42 |
43 | const supportedMethodsMiddleware = container.resolve(SupportedMethodsMiddleware);
44 | const app: express.Application = express()
45 | .use('/methods/:method/verifiers', (req: express.Request, res: express.Response, next: express.NextFunction) => supportedMethodsMiddleware.execute(req, res, next))
46 | .use('/methods/:method/verifiers', (req: express.Request, res: express.Response, next: express.NextFunction) => res.status(200).json({}));
47 |
48 | it('will not accept unsupported method', (done) => {
49 | request(app).post('/methods/unsupported/verifiers')
50 | .end((err, res) => {
51 | expect(res.status).to.equal(404);
52 | expect(res.body.error).to.equal('Method not supported');
53 | done();
54 | });
55 | });
56 |
57 | it('will accept email method', (done) => {
58 | request(app).post('/methods/email/verifiers')
59 | .query({'method': 'email'})
60 | .end((err, res) => {
61 | expect(res.status).to.equal(200);
62 | done();
63 | });
64 | });
65 |
66 | });
67 |
68 | });
69 |
--------------------------------------------------------------------------------
/src/middlewares/specs/requests.spec.ts:
--------------------------------------------------------------------------------
1 | import * as express from 'express';
2 | import * as chai from 'chai';
3 | import { validateRequest } from '../requests';
4 | import * as bodyParser from 'body-parser';
5 |
6 | chai.use(require('chai-http'));
7 | const {expect, request} = chai;
8 |
9 | function createApp() {
10 | const app: express.Application = express();
11 | return app.use(bodyParser.json());
12 | }
13 |
14 | function createRequest(app: express.Application, data: any) {
15 | return request(app).post('/').send(data);
16 | }
17 |
18 | describe('Requests Middlewares', () => {
19 |
20 | describe('ValidateRequest validator', () => {
21 | let app = createApp()
22 | .use((req: express.Request, res: express.Response, next: express.NextFunction) => validateRequest(req, res, next));
23 |
24 | it('will accept right filled request', (done) => {
25 | createRequest(app, {
26 | code: 'code'
27 | }).end((err, res) => {
28 | expect(res.status).to.equal(404);
29 | done();
30 | });
31 | });
32 |
33 | it('will not accept request with invalid fields', (done) => {
34 | createRequest(app, {
35 | code: 123
36 | }).end((err, res) => {
37 | expect(res.status).to.equal(422);
38 | done();
39 | });
40 | });
41 |
42 | it('will not accept request without required fields', (done) => {
43 | request(app).post('/').send({}).end((err, res) => {
44 | expect(res.status).to.equal(422);
45 | done();
46 | });
47 | });
48 |
49 | });
50 |
51 | });
52 |
--------------------------------------------------------------------------------
/src/server.ts:
--------------------------------------------------------------------------------
1 | import * as express from 'express';
2 | import { Response, Request, NextFunction, Application } from 'express';
3 | import * as bodyParser from 'body-parser';
4 | import { InversifyExpressServer } from 'inversify-express-utils';
5 |
6 | import config from './config';
7 | import { container } from './ioc.container';
8 |
9 | const app: Application = express();
10 |
11 | app.disable('x-powered-by');
12 |
13 | app.use((req: Request, res: Response, next: NextFunction) => {
14 | if (config.server.forceHttps === 'enabled') {
15 | if (!req.secure) {
16 | return res.redirect('https://' + req.hostname + ':' + config.server.httpsPort + req.originalUrl);
17 | }
18 |
19 | res.setHeader('Strict-Transport-Security', 'max-age=31536000');
20 | }
21 |
22 | const acceptHeader = req.header('Accept') || '';
23 |
24 | if (acceptHeader !== 'application/json' && acceptHeader.indexOf('application/vnd.jincor+json;') !== 0) {
25 | return res.status(406).json({
26 | error: 'Unsupported "Accept" header'
27 | });
28 | }
29 |
30 | res.setHeader('X-Content-Type-Options', 'nosniff');
31 | res.setHeader('X-Frame-Options', 'deny');
32 | res.setHeader('Content-Security-Policy', 'default-src \'none\'');
33 | return next();
34 | });
35 |
36 | app.post('*', (req: Request, res: Response, next: NextFunction) => {
37 | if (req.header('Content-Type') !== 'application/json') {
38 | return res.status(406).json({
39 | error: 'Unsupported "Content-Type"'
40 | });
41 | }
42 |
43 | return next();
44 | });
45 |
46 | app.use(bodyParser.json());
47 | app.use(bodyParser.urlencoded({ extended: false }));
48 |
49 | let server = new InversifyExpressServer(container, null, null, app);
50 |
51 | export default server.build();
52 |
--------------------------------------------------------------------------------
/src/services/auth.ts:
--------------------------------------------------------------------------------
1 | import * as LRU from 'lru-cache';
2 | import { injectable } from 'inversify';
3 | import * as request from 'request';
4 | import 'reflect-metadata';
5 |
6 | import config from '../config';
7 |
8 | export const AuthenticationServiceType = Symbol('AuthenticationServiceType');
9 |
10 | export class AuthenticationException extends Error { }
11 |
12 | /**
13 | * AuthenticationService interface
14 | */
15 | export interface AuthenticationService {
16 |
17 | validate(jwtToken: string): Promise;
18 |
19 | }
20 |
21 | /**
22 | * ExternalHttpJwtAuthenticationService class
23 | */
24 | @injectable()
25 | export class ExternalHttpJwtAuthenticationService implements AuthenticationService {
26 |
27 | private apiUrl: string = config.auth.url;
28 | private timeout: number = config.auth.timeout;
29 |
30 | /**
31 | * Validate JWT token
32 | * @param jwtToken
33 | */
34 | async validate(jwtToken: string): Promise {
35 | if (!jwtToken) {
36 | return null;
37 | }
38 |
39 | return await this.callVerifyJwtTokenMethodEndpoint(jwtToken);
40 | }
41 |
42 | /**
43 | * Make HTTP/HTTPS request
44 | * @param jwtToken
45 | */
46 | private async callVerifyJwtTokenMethodEndpoint(jwtToken: string): Promise {
47 | /* istanbul ignore next */
48 | return new Promise((resolve, reject) => {
49 | request.post({
50 | url: this.apiUrl,
51 | timeout: this.timeout,
52 | headers: {
53 | 'accept': 'application/json'
54 | },
55 | json: { token: jwtToken }
56 | // agentOptions: {
57 | // cert: '',
58 | // key: '',
59 | // passphrase: '',
60 | // securityOptions: 'SSL_OP_NO_SSLv3'
61 | // }
62 | }, (error: any, response: any, body: any) => {
63 | if (error) {
64 | return reject(new AuthenticationException(error));
65 | }
66 |
67 | if (response.statusCode !== 200 || !body.decoded) {
68 | return reject(new AuthenticationException('Invalid token'));
69 | }
70 |
71 | if (!body.decoded.isTenant) {
72 | return reject(new AuthenticationException('JWT isn\'t type of tenant'));
73 | }
74 |
75 | resolve(body.decoded);
76 | });
77 | });
78 | }
79 |
80 | }
81 |
82 | /**
83 | * Simple, single value token validator
84 | * @internal
85 | */
86 | @injectable()
87 | export class JwtSingleInlineAuthenticationService implements AuthenticationService {
88 |
89 | private storedToken: string = 'TOKEN';
90 |
91 | /**
92 | * Set token
93 | * @param token
94 | */
95 | public setToken(token: string) {
96 | this.storedToken = token;
97 | }
98 |
99 | /**
100 | * Validate JWT token
101 | * @param jwtToken
102 | */
103 | async validate(jwtToken: string): Promise {
104 | if (jwtToken !== this.storedToken) {
105 | return null;
106 | }
107 | return {
108 | id: 'tenantId',
109 | isTenant: true,
110 | login: 'tenantLogin',
111 | jti: '1234',
112 | iat: 1234,
113 | aud: '123'
114 | };
115 | }
116 | }
117 |
118 | /**
119 | * Cache decorator for only successfully request
120 | */
121 | export class CachedAuthenticationDecorator implements AuthenticationService {
122 | private lruCache: any;
123 |
124 | /**
125 | * @param authenticationService
126 | * @param maxAgeInSeconds
127 | * @param maxLength
128 | */
129 | constructor(private authenticationService: AuthenticationService, maxAgeInSeconds: number = 30, maxLength: number = 1000) {
130 | this.lruCache = LRU({
131 | max: maxLength,
132 | maxAge: maxAgeInSeconds * 1000
133 | });
134 | }
135 |
136 | /**
137 | * @inheritdoc
138 | */
139 | async validate(jwtToken: string): Promise {
140 | try {
141 | if (this.lruCache.has(jwtToken)) {
142 | return this.lruCache.get(jwtToken);
143 | }
144 |
145 | const result = await this.authenticationService.validate(jwtToken);
146 | this.lruCache.set(jwtToken, result);
147 | return result;
148 | } catch (err) {
149 | throw err;
150 | }
151 | }
152 | }
153 |
--------------------------------------------------------------------------------
/src/services/authenticator.verification.ts:
--------------------------------------------------------------------------------
1 | import * as authenticator from 'authenticator';
2 | import { NotFoundException } from '../exceptions/exceptions';
3 | import { BaseVerificationService } from './base.verification';
4 |
5 | export default class AuthenticatorVerificationService extends BaseVerificationService {
6 | async initiate(params: any, tenantData: TenantVerificationResult): Promise {
7 | const result = await super.initiate(params, tenantData);
8 |
9 | let secret = await this.getSecret(params.consumer, tenantData.id);
10 |
11 | if (secret === null || !secret.verified) {
12 | secret = await this.generateAndStoreSecret(params.consumer, tenantData.id);
13 | result.totpUri = authenticator.generateTotpUri(secret.secret, params.consumer, params.issuer, 'SHA1', 6, 30);
14 | }
15 | return result;
16 | }
17 |
18 | async validate(verificationId: string, params: any, tenantData: TenantVerificationResult): Promise {
19 | const verification = await this.getVerification(verificationId);
20 | if (verification === null) {
21 | throw new NotFoundException('Verification is not found');
22 | }
23 |
24 | const secret = await this.getSecret(verification.consumer, tenantData.id);
25 | if (secret === null) {
26 | throw new NotFoundException('User secret is not found');
27 | }
28 |
29 | const result = authenticator.verifyToken(secret.secret, params.code.toString());
30 | if (!result) {
31 | verification.attempts += 1;
32 | await this.storageService.set(`${ this.keyPrefix }${ verificationId }`, verification);
33 |
34 | return {
35 | isValid: false,
36 | verification
37 | };
38 | }
39 |
40 | if (params.removeSecret === true) {
41 | await this.removeSecret(verification.consumer, tenantData.id);
42 | }
43 |
44 | if (!secret.verified) {
45 | await this.verifyAndSaveSecret(verification.consumer, tenantData.id, secret);
46 | }
47 |
48 | await this.storageService.remove(`${ this.keyPrefix }${ verificationId }`);
49 | return {
50 | isValid: true,
51 | verification
52 | };
53 | }
54 |
55 | private async generateAndStoreSecret(consumer: string, tenantId: string): Promise {
56 | const secret = {
57 | secret: authenticator.generateKey(),
58 | verified: false
59 | };
60 | await this.storageService.set(`${ this.keyPrefix + tenantId + consumer }`, secret);
61 | return secret;
62 | }
63 |
64 | private async verifyAndSaveSecret(consumer: string, tenantId: string, secret: AuthenticatorSecret): Promise {
65 | secret.verified = true;
66 | await this.storageService.set(`${ this.keyPrefix + tenantId + consumer }`, secret);
67 | }
68 |
69 | private async getSecret(consumer: string, tenantId: string): Promise {
70 | return await this.storageService.get(`${ this.keyPrefix + tenantId + consumer }`, null);
71 | }
72 |
73 | private async removeSecret(consumer: string, tenantId: string): Promise {
74 | await this.storageService.remove(`${ this.keyPrefix + tenantId + consumer }`);
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/services/base.verification.ts:
--------------------------------------------------------------------------------
1 | import * as moment from 'moment';
2 | import * as uuid from 'node-uuid';
3 | import { NotFoundException, InvalidParametersException } from '../exceptions/exceptions';
4 | import * as crypto from 'crypto';
5 |
6 | /**
7 | * BaseVerificationService
8 | */
9 | export abstract class BaseVerificationService implements VerificationService {
10 |
11 | /**
12 | * Base constructor for verification service
13 | *
14 | * @param keyPrefix
15 | * @param storageService
16 | */
17 | constructor(protected keyPrefix: string, protected storageService: StorageService) {
18 | }
19 |
20 | /**
21 | * Get or generate verificationId
22 | *
23 | * @param policyParams
24 | */
25 | protected getVerificationId(policyParams: PolicyParamsType): string {
26 | if (policyParams.forcedVerificationId) {
27 | // @TODO: Add validation with usage of Joi
28 | return policyParams.forcedVerificationId;
29 | }
30 |
31 | return uuid.v4();
32 | }
33 |
34 | /**
35 | * Get or generate code
36 | *
37 | * @param generateParams
38 | * @param policyParams
39 | */
40 | protected getCode(generateParams: GenerateCodeType, policyParams: PolicyParamsType): string {
41 | if (policyParams && policyParams.forcedCode) {
42 | // @TODO: Add validation with usage of Joi
43 | return policyParams.forcedCode;
44 | }
45 |
46 | if (generateParams) {
47 | return this.generateCode(generateParams.symbolSet, generateParams.length);
48 | }
49 |
50 | return '';
51 | }
52 |
53 | /**
54 | * Initiate verification process
55 | *
56 | * @param params
57 | * @param tenantData
58 | */
59 | async initiate(params: ParamsType, tenantData: TenantVerificationResult): Promise {
60 | const verificationId = this.getVerificationId(params.policy);
61 |
62 | const code = this.getCode(params.generateCode, params.policy);
63 |
64 | const ttlInSeconds = moment.duration(params.policy.expiredOn).asSeconds();
65 |
66 | if (!ttlInSeconds) {
67 | throw new InvalidParametersException('expiredOn format is invalid');
68 | }
69 |
70 | const data = {
71 | verificationId,
72 | consumer: params.consumer,
73 | payload: params.payload,
74 | code,
75 | attempts: 0,
76 | expiredOn: ~~((+new Date() + ttlInSeconds * 1000) / 1000)
77 | };
78 |
79 | await this.storageService.set(this.keyPrefix + verificationId, data, { ttlInSeconds });
80 |
81 | return data;
82 | }
83 |
84 | /**
85 | * Validate verificationId with passed code
86 | *
87 | * @param verificationId
88 | * @param params
89 | * @param tenantData
90 | */
91 | async validate(verificationId: string, params: any, tenantData: TenantVerificationResult): Promise {
92 | const result = await this.getVerification(verificationId);
93 |
94 | if (result === null) {
95 | throw new NotFoundException();
96 | }
97 |
98 | if (result.code === params.code) {
99 | await this.remove(verificationId);
100 | delete result.code;
101 | return {
102 | isValid: true,
103 | verification: result
104 | };
105 | }
106 |
107 | result.attempts += 1;
108 | await this.storageService.set(this.keyPrefix + verificationId, result);
109 |
110 | delete result.code;
111 | return {
112 | isValid: false,
113 | verification: result
114 | };
115 | }
116 |
117 | /**
118 | * Remove verificationId
119 | *
120 | * @param verificationId
121 | */
122 | async remove(verificationId: string): Promise {
123 | const result = await this.storageService.remove(this.keyPrefix + verificationId);
124 | return result !== null;
125 | }
126 |
127 | async getVerification(verificationId: string): Promise {
128 | return await this.storageService.get(this.keyPrefix + verificationId, null);
129 | }
130 |
131 | // @TODO: Rethink, may be is too weak algorithm here
132 | protected generateCode(symbolSet: Array, length: number): string {
133 | let stringWithAllSymbols = '';
134 | let resultCode = '';
135 |
136 | stringWithAllSymbols = (symbolSet || []).map(element => {
137 | switch (element) {
138 | case 'alphas':
139 | return 'abcdefghijklmnopqrstuvwxyz'; // ~27%
140 | case 'ALPHAS':
141 | return 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; // ~27%
142 | case 'DIGITS':
143 | return '01234567890123456789'; // ~22% it's duplicated for uniform distribution
144 | case 'SYMBOLS':
145 | return '!@#$%^&*-+=|<>()[]{};:_'; // ~24%
146 | default:
147 | return element;
148 | }
149 | }).join('');
150 |
151 | while (length--) {
152 | resultCode += stringWithAllSymbols[crypto.randomBytes(1)[0] % stringWithAllSymbols.length];
153 | }
154 |
155 | return resultCode;
156 | }
157 | }
158 |
--------------------------------------------------------------------------------
/src/services/email.verification.ts:
--------------------------------------------------------------------------------
1 | import { EmailProviderService, EmailProvider } from './providers/index';
2 | import { BaseVerificationService } from './base.verification';
3 | import config from '../config';
4 | import { InvalidParametersException } from '../exceptions/exceptions';
5 | import * as Joi from 'joi';
6 | import { validateObjectByJoiScheme } from './helpers';
7 |
8 | export const jsonSchemeInitiateRequestValidEmailConsumer = Joi.object().keys({
9 | consumer: Joi.string().email()
10 | });
11 |
12 | export const jsonSchemeInitiateRequestEmailTemplate = Joi.object().keys({
13 | fromEmail: Joi.string().empty(),
14 | fromName: Joi.string().empty(),
15 | subject: Joi.string().empty(),
16 | body: Joi.string().empty().required()
17 | });
18 |
19 | /**
20 | * Concrete EmailVerificationService.
21 | */
22 | export default class EmailVerificationService extends BaseVerificationService {
23 |
24 | protected emailProvider: EmailProvider;
25 |
26 | /**
27 | * Email verification specialization
28 | *
29 | * @param keyPrefix
30 | * @param storageService
31 | * @param emailProviderService
32 | */
33 | constructor(
34 | protected keyPrefix: string,
35 | protected storageService: StorageService,
36 | protected emailProviderService: EmailProviderService
37 | ) {
38 | super(keyPrefix, storageService);
39 |
40 | if (!config.providers.email.provider) {
41 | throw new InvalidParametersException(`The environment variable MAIL_DRIVER isn\'t set up`);
42 | }
43 |
44 | this.emailProvider = emailProviderService.getEmailProviderByName(config.providers.email.provider);
45 | }
46 |
47 | /**
48 | * @inheritdoc
49 | */
50 | async initiate(params: ParamsType, tenantData: TenantVerificationResult): Promise {
51 | if (params.consumer) {
52 | validateObjectByJoiScheme(params, jsonSchemeInitiateRequestValidEmailConsumer);
53 | }
54 | validateObjectByJoiScheme(params.template, jsonSchemeInitiateRequestEmailTemplate);
55 |
56 | const templateParams: EmailTemplateType = params.template;
57 | let responseObject = await super.initiate(params, tenantData);
58 |
59 | let emailFrom = templateParams.fromEmail || '';
60 |
61 | if (templateParams.fromName) {
62 | emailFrom = `${templateParams.fromName} <${emailFrom}>`;
63 | }
64 |
65 | // @TODO: The better solution is used external microservice for sending
66 | await this.emailProvider.send(
67 | emailFrom,
68 | [params.consumer],
69 | templateParams.subject || 'Verification Email',
70 | templateParams.body
71 | .replace(/{{{CODE}}}/g, responseObject.code)
72 | .replace(/{{{VERIFICATION_ID}}}/g, responseObject.verificationId)
73 | );
74 |
75 | delete responseObject.code;
76 |
77 | return responseObject;
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/src/services/helpers.ts:
--------------------------------------------------------------------------------
1 | import * as Joi from 'joi';
2 | import { InvalidParametersException } from '../exceptions/exceptions';
3 |
4 | export function validateObjectByJoiScheme(obj: any, scheme: Joi.Schema) {
5 | const result = Joi.validate(obj || {}, scheme, { allowUnknown: true });
6 | if (result.error) {
7 | throw new InvalidParametersException(result.error && result.error.message || result.error);
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/services/providers/email.ts:
--------------------------------------------------------------------------------
1 | import * as mailgun from 'mailgun-js';
2 | import * as mailcomposer from 'mailcomposer';
3 |
4 | import {
5 | EmailProvider,
6 | NotProperlyInitializedException,
7 | InvalidParametersException,
8 | InternalProviderException
9 | } from './index';
10 |
11 | /**
12 | * Dummy Email Provider class
13 | */
14 | export class DummyEmailProvider implements EmailProvider {
15 |
16 | /**
17 | * Get name of concrete provider
18 | */
19 | public static getName() {
20 | return 'dummy';
21 | }
22 |
23 | /**
24 | * @inheritdoc
25 | */
26 | public send(sender: string, recipients: Array, subject: string, text: string): Promise {
27 | console.log('SEND EMAIL HEAD: sender=%s recipients=%s subject=%s', sender, recipients, subject);
28 | console.log('SEND EMAIL BODY:', text);
29 | return Promise.resolve({sender, recipients, subject});
30 | }
31 | }
32 |
33 | /**
34 | * Mailgun Email Provider class
35 | */
36 | export class MailgunEmailProvider implements EmailProvider {
37 | private api: any;
38 |
39 | /**
40 | * Initiate concrete provider instance
41 | */
42 | constructor(config: any) {
43 | const {
44 | MAILGUN_SECRET,
45 | MAILGUN_DOMAIN
46 | } = config;
47 |
48 | if (!MAILGUN_DOMAIN) {
49 | throw new NotProperlyInitializedException('MAILGUN_DOMAIN is invalid');
50 | }
51 |
52 | if (!MAILGUN_SECRET || !/^(api:)?key-.+/.test(MAILGUN_SECRET)) {
53 | throw new NotProperlyInitializedException('MAILGUN_SECRET is invalid');
54 | }
55 |
56 | this.api = this.createMailgunApi({
57 | apiKey: MAILGUN_SECRET,
58 | domain: MAILGUN_DOMAIN
59 | });
60 | }
61 |
62 | /**
63 | * Create instance for mailgun api
64 | * @param config
65 | */
66 | protected createMailgunApi(config: any) {
67 | return new mailgun(config);
68 | }
69 |
70 | /**
71 | * Get name of concrete provider
72 | */
73 | public static getName() {
74 | return 'mailgun';
75 | }
76 |
77 | /**
78 | * @inheritdoc
79 | */
80 | public send(sender: string, recipients: Array, subject: string, text: string): Promise {
81 | if (!recipients.length) {
82 | throw new InvalidParametersException();
83 | }
84 |
85 | /* istanbul ignore next */
86 | return new Promise((resolve, reject) => {
87 | let mail = mailcomposer({
88 | from: sender,
89 | to: recipients[0],
90 | subject,
91 | html: text
92 | });
93 |
94 | mail.build((mailBuildError, message) => {
95 | let dataToSend = {
96 | to: recipients[0],
97 | message: message.toString('ascii')
98 | };
99 |
100 | this.api.messages().sendMime(dataToSend, (err, body) => {
101 | if (err) {
102 | reject(new InternalProviderException(err));
103 | }
104 | resolve(body);
105 | });
106 | });
107 | });
108 | }
109 | }
110 |
111 | export class MailjetEmailProvider implements EmailProvider {
112 | private api: any;
113 |
114 | /**
115 | * Initiate concrete provider instance
116 | */
117 | constructor(config: any) {
118 | this.api = require('node-mailjet').connect(config.MAILJET_API_KEY, config.MAILJET_API_SECRET);
119 | }
120 |
121 | /**
122 | * @inheritdoc
123 | */
124 | public send(sender: string, recipients: string[], subject: string, text: string): Promise {
125 | const sendEmail = this.api.post('send');
126 |
127 | const emailData = {
128 | 'FromEmail': sender,
129 | 'Subject': subject,
130 | 'Html-part': text,
131 | 'Recipients': [{'Email': recipients[0]}]
132 | };
133 |
134 | return sendEmail.request(emailData);
135 | }
136 |
137 | public static getName() {
138 | return 'mailjet';
139 | }
140 | }
141 |
--------------------------------------------------------------------------------
/src/services/providers/index.ts:
--------------------------------------------------------------------------------
1 | import config from '../../config';
2 | import * as emailProviders from './email';
3 | import { injectable } from 'inversify';
4 | import 'reflect-metadata';
5 |
6 | // Exceptions
7 | export class ProviderException extends Error { }
8 | export class NotProperlyInitializedException extends ProviderException { }
9 | export class InvalidParametersException extends ProviderException { }
10 | export class NotFoundException extends ProviderException { }
11 | export class InternalProviderException extends ProviderException { }
12 |
13 | // Types
14 |
15 | /**
16 | * Email Provider interface
17 | */
18 | export interface EmailProvider {
19 |
20 | /**
21 | * Send an email
22 | *
23 | * @param sender
24 | * @param recipients
25 | * @param subject
26 | * @param text
27 | * @return Promise
28 | */
29 | send(sender: string, recipients: Array, subject: string, text: string): Promise;
30 | }
31 |
32 | export const EmailProviderServiceType = Symbol('EmailProviderService');
33 |
34 | /**
35 | * Email Providers service
36 | */
37 | @injectable()
38 | export class EmailProviderService {
39 | private emailRegistry: {
40 | [key: string]: EmailProvider
41 | } = {};
42 |
43 | /**
44 | * Get concrete email provider by name
45 | * @param name
46 | */
47 | public getEmailProviderByName(name: string): EmailProvider {
48 | return this.emailRegistry[name] || (
49 | this.emailRegistry[name] = this.findAndInstantiate(name, emailProviders)
50 | );
51 | }
52 |
53 | /**
54 | * Find by name provider
55 | * @param name
56 | */
57 | private findAndInstantiate(name: string, providers: any): EmailProvider {
58 | let providerName = Object.keys(providers).filter((providerClassName) => (
59 | providers[providerClassName] instanceof Object &&
60 | providers[providerClassName].hasOwnProperty('getName') &&
61 | providers[providerClassName].getName() === name
62 | ));
63 |
64 | if (providerName.length !== 1) {
65 | throw new NotFoundException(`${name} not found or it\'s duplicated`);
66 | }
67 |
68 | return new providers[providerName[0]](process.env);
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/services/providers/specs/email.spec.ts:
--------------------------------------------------------------------------------
1 | import * as express from 'express';
2 | import * as chai from 'chai';
3 | import * as sinon from 'sinon';
4 |
5 | import { DummyEmailProvider, MailgunEmailProvider } from '../email';
6 | import { InvalidParametersException } from '../index';
7 |
8 | const {expect} = chai;
9 |
10 | describe('Email Providers', () => {
11 |
12 | describe('Test DummyEmailProvider', () => {
13 |
14 | it('will get dummy name', async() => {
15 | expect(DummyEmailProvider.getName()).is.equals('dummy');
16 | });
17 |
18 | it('will send an email', async() => {
19 | let instance = new DummyEmailProvider();
20 | expect(await instance.send(
21 | 'test@test.com', ['test1@test1.com'], 'subject', 'email body'
22 | )).is.property('sender').equals('test@test.com');
23 | });
24 |
25 | });
26 |
27 | describe('Test MailgunEmailProvider', () => {
28 |
29 | const stubMailGunApi = {
30 | messages: () => stubMailGunApi,
31 | sendMime: (dataToSend, callback) => {
32 | callback(null, dataToSend.message);
33 | }
34 | };
35 |
36 | class StubMailgunEmailProvider extends MailgunEmailProvider {
37 | protected createMailgunApi(config: any) {
38 | return stubMailGunApi;
39 | }
40 | }
41 |
42 | it('will get mailgun name', async() => {
43 | expect(MailgunEmailProvider.getName()).is.equals('mailgun');
44 | });
45 |
46 | it('will throw NotProperlyInitializedException if incorrect MAILGUN_SECRET passed', async() => {
47 | expect(() => {
48 | const stubInstance = new StubMailgunEmailProvider({
49 | MAILGUN_SECRET: 'secret-12345',
50 | MAILGUN_DOMAIN: 'domain.com'
51 | });
52 | }).throws('MAILGUN_SECRET is invalid');
53 | });
54 |
55 | it('will throw NotProperlyInitializedException if incorrect MAILGUN_DOMAIN not passed', async() => {
56 | expect(() => {
57 | const stubInstance = new StubMailgunEmailProvider({
58 | MAILGUN_SECRET: 'key-12345'
59 | });
60 | }).throws('MAILGUN_DOMAIN is invalid');
61 | });
62 |
63 | it('will send an email', async() => {
64 | const stubInstance = new StubMailgunEmailProvider({
65 | MAILGUN_SECRET: 'key-12345',
66 | MAILGUN_DOMAIN: 'domain.com'
67 | });
68 |
69 | const bodyString = 'MESSAGE';
70 |
71 | expect(await stubInstance.send('sender@sender.com', ['recipient@recipient.com'], 'subject', bodyString))
72 | .is.contains(bodyString);
73 | });
74 |
75 | });
76 |
77 | });
78 |
--------------------------------------------------------------------------------
/src/services/specs/auth.spec.ts:
--------------------------------------------------------------------------------
1 | import { AuthenticationService, AuthenticationServiceType, ExternalHttpJwtAuthenticationService, JwtSingleInlineAuthenticationService, CachedAuthenticationDecorator } from '../auth';
2 | import * as chai from 'chai';
3 | import * as sinon from 'sinon';
4 |
5 | import { container } from '../../ioc.container';
6 |
7 | const {expect} = chai;
8 |
9 | const tenantData = {
10 | id: 'tenantId',
11 | isTenant: true,
12 | login: 'tenantLogin',
13 | jti: '1234',
14 | iat: 1234,
15 | aud: '123'
16 | };
17 |
18 | describe('Auth Services', () => {
19 |
20 | describe('Test JwtSingleInlineAuthenticationService', () => {
21 | let instance: AuthenticationService;
22 | container.rebind(AuthenticationServiceType)
23 | .to(JwtSingleInlineAuthenticationService);
24 |
25 | beforeEach(() => {
26 | instance = container.resolve(JwtSingleInlineAuthenticationService);
27 | });
28 |
29 | it('will not validate empty JWT string', async() => {
30 | expect(await instance.validate('')).is.eq(null);
31 | });
32 |
33 | it('will not validate with incorrect JWT string', async() => {
34 | expect(await instance.validate('not empty string')).is.eq(null);
35 | });
36 |
37 | it('will successfully validate with valid JWT string', async() => {
38 | expect(await instance.validate('TOKEN')).deep.eq(tenantData);
39 | });
40 | });
41 |
42 | describe('Test ExternalHttpJwtAuthenticationService', () => {
43 | let instance: AuthenticationService;
44 | container.rebind(AuthenticationServiceType)
45 | .to(ExternalHttpJwtAuthenticationService);
46 |
47 | beforeEach(async() => {
48 | instance = container.resolve(ExternalHttpJwtAuthenticationService);
49 | });
50 |
51 | it('will not validate empty JWT string', async() => {
52 | let mock = sinon.mock(instance);
53 | mock.expects('callVerifyJwtTokenMethodEndpoint').never();
54 | expect(await instance.validate('')).is.eq(null);
55 | mock.verify();
56 | });
57 |
58 | it('will successfully validate with valid JWT string', async() => {
59 | let mock = sinon.mock(instance);
60 | mock.expects('callVerifyJwtTokenMethodEndpoint').once().returns(Promise.resolve(tenantData));
61 | expect(await instance.validate('not empty string')).deep.eq(tenantData);
62 | mock.verify();
63 | });
64 | });
65 |
66 | describe('Test CachedAuthenticationDecorator', () => {
67 | let mock;
68 |
69 | function createDecoratedInstance(returnResult: TenantVerificationResult) {
70 | let instance = {
71 | validate: (jwtToken: string): Promise => Promise.resolve(returnResult)
72 | };
73 | mock = sinon.mock(instance);
74 | mock.expects('validate').once().returns(Promise.resolve(returnResult));
75 |
76 | return new CachedAuthenticationDecorator(instance);
77 | }
78 |
79 | it('will store a success validation result', async() => {
80 | let decoratedInstance = createDecoratedInstance(tenantData);
81 |
82 | expect(await decoratedInstance.validate('TOKEN')).is.eq(tenantData);
83 | expect(await decoratedInstance.validate('TOKEN')).is.eq(tenantData);
84 |
85 | mock.verify();
86 | });
87 |
88 | it('will store a failed validation result', async() => {
89 | let decoratedInstance = createDecoratedInstance(null);
90 |
91 | expect(await decoratedInstance.validate('TOKEN')).is.eq(null);
92 | expect(await decoratedInstance.validate('TOKEN')).is.eq(null);
93 |
94 | mock.verify();
95 | });
96 | });
97 |
98 | });
99 |
--------------------------------------------------------------------------------
/src/services/specs/authenticator.spec.ts:
--------------------------------------------------------------------------------
1 | import * as chai from 'chai';
2 | import AuthenticatorVerificationService from '../authenticator.verification';
3 | import { SimpleInMemoryStorageService } from '../storages';
4 | import * as TypeMoq from 'typemoq';
5 | import * as authenticator from 'authenticator';
6 |
7 | chai.use(require('chai-as-promised'));
8 | const { expect } = chai;
9 |
10 | const tenantData = {
11 | id: 'tenantId',
12 | isTenant: true,
13 | login: 'tenantLogin',
14 | jti: '1234',
15 | iat: 1234,
16 | aud: '123'
17 | };
18 |
19 | describe('Authenticator service', () => {
20 | describe('Initiate', () => {
21 | it('should initiate verification - also generate secret for consumer if no secret yet', async() => {
22 | const storageMock = TypeMoq.Mock.ofType(SimpleInMemoryStorageService);
23 | storageMock.setup(x => x.get(TypeMoq.It.isAny(), TypeMoq.It.isAny()))
24 | .returns(async(): Promise => null);
25 |
26 | this.authenticator = new AuthenticatorVerificationService('test', storageMock.object);
27 |
28 | const input = {
29 | consumer: 'test@test.com',
30 | issuer: 'Jincor',
31 | policy: {
32 | expiredOn: '01:00:00',
33 | forcedCode: '123'
34 | }
35 | };
36 |
37 | const result = await this.authenticator.initiate(input, tenantData);
38 | expect(result.totpUri).contains('otpauth://totp/Jincor:test@test.com?secret=');
39 | storageMock.verify(
40 | x => x.set(TypeMoq.It.isValue('testtenantIdtest@test.com'), TypeMoq.It.isAny()),
41 | TypeMoq.Times.once()
42 | );
43 | });
44 |
45 | it('should initiate verification - also generate secret for consumer if old secret is not verified', async() => {
46 | const secret = {
47 | secret: '3qlq j5uj gdcj xoqt 6rhu yglx 5mf5 i7ll',
48 | verified: false
49 | };
50 |
51 | const storageMock = TypeMoq.Mock.ofType(SimpleInMemoryStorageService);
52 | storageMock.setup(x => x.get(TypeMoq.It.isAny(), TypeMoq.It.isAny()))
53 | .returns(async(): Promise => secret);
54 |
55 | this.authenticator = new AuthenticatorVerificationService('test', storageMock.object);
56 |
57 | const input = {
58 | consumer: 'test@test.com',
59 | issuer: 'Jincor',
60 | policy: {
61 | expiredOn: '01:00:00',
62 | forcedCode: '123'
63 | }
64 | };
65 |
66 | const result = await this.authenticator.initiate(input, tenantData);
67 | expect(result.totpUri).contains('otpauth://totp/Jincor:test@test.com?secret=');
68 | storageMock.verify(x => x.set(TypeMoq.It.isValue('testtenantIdtest@test.com'), TypeMoq.It.isAny()), TypeMoq.Times.once());
69 | });
70 |
71 | it('should initiate verification - use existing secret', async() => {
72 | const storageMock = TypeMoq.Mock.ofType(SimpleInMemoryStorageService);
73 | const secret = {
74 | secret: '3qlq j5uj gdcj xoqt 6rhu yglx 5mf5 i7ll',
75 | verified: true
76 | };
77 |
78 | storageMock.setup(x => x.get(TypeMoq.It.isAny(), TypeMoq.It.isAny()))
79 | .returns(async(): Promise => secret);
80 |
81 | this.authenticator = new AuthenticatorVerificationService('test', storageMock.object);
82 |
83 | const input = {
84 | consumer: 'test@test.com',
85 | issuer: 'Jincor',
86 | policy: {
87 | expiredOn: '01:00:00',
88 | forcedCode: '123'
89 | }
90 | };
91 |
92 | const result = await this.authenticator.initiate(input, tenantData);
93 | expect(result).to.not.have.property('totpUri');
94 | });
95 |
96 | describe('Validate', () => {
97 | it('should validate verification - correct code', async() => {
98 | const secret = {
99 | secret: '3qlq j5uj gdcj xoqt 6rhu yglx 5mf5 i7ll',
100 | verified: true
101 | };
102 | const verificationId = 'verificationId';
103 | const verificationData = {
104 | consumer: 'test@test.com'
105 | };
106 |
107 | const storageMock = TypeMoq.Mock.ofType(SimpleInMemoryStorageService);
108 | storageMock.setup(x => x.get(TypeMoq.It.isValue(`test${ verificationId }`), TypeMoq.It.isAny()))
109 | .returns(async(): Promise => verificationData);
110 |
111 | storageMock.setup(x => x.get(TypeMoq.It.isValue('testtenantIdtest@test.com'), TypeMoq.It.isAny()))
112 | .returns(async(): Promise => secret);
113 |
114 | this.authenticator = new AuthenticatorVerificationService('test', storageMock.object);
115 |
116 | const input = {
117 | consumer: 'test@test.com',
118 | code: authenticator.generateToken(secret.secret)
119 | };
120 |
121 | const result = await this.authenticator.validate(verificationId, input, tenantData);
122 | expect(result.isValid).to.eq(true);
123 | });
124 |
125 | it('should validate verification - correct code, should verify secret if not verified yet', async() => {
126 | const secret = {
127 | secret: '3qlq j5uj gdcj xoqt 6rhu yglx 5mf5 i7ll',
128 | verified: false
129 | };
130 |
131 | const verificationId = 'verificationId';
132 | const verificationData = {
133 | consumer: 'test@test.com'
134 | };
135 |
136 | const storageMock = TypeMoq.Mock.ofType(SimpleInMemoryStorageService);
137 | storageMock.setup(x => x.get(TypeMoq.It.isValue(`test${ verificationId }`), TypeMoq.It.isAny()))
138 | .returns(async(): Promise => verificationData);
139 |
140 | storageMock.setup(x => x.get(TypeMoq.It.isValue('testtenantIdtest@test.com'), TypeMoq.It.isAny()))
141 | .returns(async(): Promise => secret);
142 |
143 | this.authenticator = new AuthenticatorVerificationService('test', storageMock.object);
144 |
145 | const input = {
146 | consumer: 'test@test.com',
147 | code: authenticator.generateToken(secret.secret)
148 | };
149 |
150 | const result = await this.authenticator.validate(verificationId, input, tenantData);
151 | expect(result.isValid).to.eq(true);
152 |
153 | const expectedSecret = {
154 | secret: '3qlq j5uj gdcj xoqt 6rhu yglx 5mf5 i7ll',
155 | verified: true
156 | };
157 |
158 | storageMock.verify(
159 | x => x.set(TypeMoq.It.isValue('testtenantIdtest@test.com'), TypeMoq.It.isValue(expectedSecret)),
160 | TypeMoq.Times.once()
161 | );
162 | });
163 |
164 | it('should validate verification - incorrect code', async() => {
165 | const secret = {
166 | secret: '3qlq j5uj gdcj xoqt 6rhu yglx 5mf5 i7ll',
167 | verified: true
168 | };
169 | const verificationId = 'verificationId';
170 | const verificationData = {
171 | consumer: 'test@test.com'
172 | };
173 |
174 | const storageMock = TypeMoq.Mock.ofType(SimpleInMemoryStorageService);
175 | storageMock.setup(x => x.get(TypeMoq.It.isValue(`test${ verificationId }`), TypeMoq.It.isAny()))
176 | .returns(async(): Promise => verificationData);
177 |
178 | storageMock.setup(x => x.get(TypeMoq.It.isValue('testtenantIdtest@test.com'), TypeMoq.It.isAny()))
179 | .returns(async(): Promise => secret);
180 |
181 | this.authenticator = new AuthenticatorVerificationService('test', storageMock.object);
182 |
183 | const input = {
184 | consumer: 'test@test.com',
185 | code: authenticator.generateToken(secret.secret) + 1
186 | };
187 |
188 | const result = await this.authenticator.validate(verificationId, input, tenantData);
189 | expect(result.isValid).to.eq(false);
190 | });
191 |
192 | it('should remove secret if param is set', async() => {
193 | const secret = {
194 | secret: '3qlq j5uj gdcj xoqt 6rhu yglx 5mf5 i7ll',
195 | verified: true
196 | };
197 | const verificationId = 'verificationId';
198 | const verificationData = {
199 | consumer: 'test@test.com'
200 | };
201 |
202 | const storageMock = TypeMoq.Mock.ofType(SimpleInMemoryStorageService);
203 | storageMock.setup(x => x.get(TypeMoq.It.isValue(`test${ verificationId }`), TypeMoq.It.isAny()))
204 | .returns(async(): Promise => verificationData);
205 |
206 | storageMock.setup(x => x.get(TypeMoq.It.isValue('testtenantIdtest@test.com'), TypeMoq.It.isAny()))
207 | .returns(async(): Promise => secret);
208 |
209 | storageMock.setup(x => x.remove(TypeMoq.It.isValue('testtenantIdtest@test.com')))
210 | .returns(async(): Promise => null);
211 |
212 | this.authenticator = new AuthenticatorVerificationService('test', storageMock.object);
213 |
214 | const input = {
215 | consumer: 'test@test.com',
216 | code: authenticator.generateToken(secret.secret),
217 | removeSecret: true
218 | };
219 |
220 | const result = await this.authenticator.validate(verificationId, input, tenantData);
221 | expect(result.isValid).to.eq(true);
222 | storageMock.verify(x => x.remove(TypeMoq.It.isValue('testtenantIdtest@test.com')), TypeMoq.Times.once());
223 | });
224 |
225 | it('should throw when verification is not found', async() => {
226 | const verificationId = 'verificationId';
227 |
228 | const storageMock = TypeMoq.Mock.ofType(SimpleInMemoryStorageService);
229 | storageMock.setup(x => x.get(TypeMoq.It.isValue(`test${ verificationId }`), TypeMoq.It.isAny()))
230 | .returns(async(): Promise => null);
231 |
232 | this.authenticator = new AuthenticatorVerificationService('test', storageMock.object);
233 |
234 | const input = {
235 | consumer: 'test@test.com',
236 | code: 123456
237 | };
238 |
239 | expect(this.authenticator.validate(verificationId, input)).to.be.rejectedWith('Verification is not found');
240 | });
241 |
242 | it('should throw when secret is not found', async() => {
243 | const verificationId = 'verificationId';
244 | const verificationData = {
245 | consumer: 'test@test.com'
246 | };
247 |
248 | const storageMock = TypeMoq.Mock.ofType(SimpleInMemoryStorageService);
249 | storageMock.setup(x => x.get(TypeMoq.It.isValue(`test${ verificationId }`), TypeMoq.It.isAny()))
250 | .returns(async(): Promise => verificationData);
251 |
252 | storageMock.setup(x => x.get(TypeMoq.It.isValue('testtenantIdtest@test.com'), TypeMoq.It.isAny()))
253 | .returns(async(): Promise => null);
254 |
255 | this.authenticator = new AuthenticatorVerificationService('test', storageMock.object);
256 |
257 | const input = {
258 | consumer: 'test@test.com',
259 | code: 123456
260 | };
261 |
262 | expect(this.authenticator.validate(verificationId, input, tenantData)).to.be.rejectedWith('User secret is not found');
263 | });
264 | });
265 | });
266 | });
267 |
--------------------------------------------------------------------------------
/src/services/specs/storages.spec.ts:
--------------------------------------------------------------------------------
1 | import * as chai from 'chai';
2 |
3 | import { SimpleInMemoryStorageService, RedisStorageService } from '../storages';
4 |
5 | const {expect} = chai;
6 |
7 | function testStorageImplementation(instance: StorageService) {
8 | it('will set value', async() => {
9 | expect(await instance.set('field', 'value', { ttlInSeconds: 10 })).is.equals('value');
10 | expect(await instance.get('field', null)).is.equals('value');
11 | });
12 |
13 | it('will try to get not existing value and return default', async() => {
14 | expect(await instance.get('noField', 'default')).is.equals('default');
15 | });
16 |
17 | it('will remove exising value', async() => {
18 | expect(await instance.set('field', 'value', { ttlInSeconds: 10 })).is.equals('value');
19 | expect(await instance.remove('field')).is.equals('value');
20 | });
21 | }
22 |
23 | describe('Storages Services', () => {
24 |
25 | describe('Test SimpleInMemoryStorageService', () => {
26 | testStorageImplementation(new SimpleInMemoryStorageService());
27 | });
28 |
29 | describe('Test RedisStorageService', () => {
30 | class StubRedisStorageService extends RedisStorageService {
31 | protected createRedisClient(): any {
32 | let items = {};
33 | return {
34 | items: {},
35 | setex: (keyName, ttl, value, callback: () => void): boolean => {
36 | items[keyName] = value;
37 | callback();
38 | return true;
39 | },
40 | get: (keyName, callback: (err, result) => void): boolean => {
41 | callback(null, items[keyName]);
42 | return true;
43 | },
44 | del: (keyName, callback: (err, result) => void): boolean => {
45 | let value = items[keyName];
46 | delete items[keyName];
47 | callback(null, value);
48 | return true;
49 | }
50 | };
51 | }
52 | }
53 | let instance = new StubRedisStorageService();
54 |
55 | testStorageImplementation(instance);
56 | });
57 |
58 | });
59 |
--------------------------------------------------------------------------------
/src/services/specs/verifications.spec.ts:
--------------------------------------------------------------------------------
1 | import { SimpleInMemoryStorageService, StorageServiceType } from '../storages';
2 | import * as chai from 'chai';
3 | import { BaseVerificationService } from '../base.verification';
4 | import { container } from '../../ioc.container';
5 | import { VerificationServiceFactoryRegister } from '../verifications';
6 | import { NotFoundException, InvalidParametersException } from '../../exceptions/exceptions';
7 | import AuthenticatorVerificationService from '../authenticator.verification';
8 | import EmailVerificationService from '../email.verification';
9 |
10 | chai.use(require('chai-as-promised'));
11 |
12 | const {expect} = chai;
13 |
14 | function createInitiateParams(params: any = {}) {
15 | return Object.assign({
16 | consumer: 'test@test.com'
17 | }, params, {
18 | policy: Object.assign({
19 | expiredOn: '01:00:00',
20 | forcedCode: '123'
21 | }, params.policy)
22 | });
23 | }
24 |
25 | const tenantData = {
26 | id: 'tenantId',
27 | isTenant: true,
28 | login: 'tenantLogin',
29 | jti: '1234',
30 | iat: 1234,
31 | aud: '123'
32 | };
33 |
34 | describe('Verifications Services', () => {
35 | container.rebind(StorageServiceType)
36 | .to(SimpleInMemoryStorageService);
37 |
38 | describe('Test VerificationServiceFactoryRegister', () => {
39 | let factory = container.resolve(VerificationServiceFactoryRegister);
40 |
41 | it('will have email method', () => {
42 | expect(factory.hasMethod('email')).is.eq(true);
43 | });
44 |
45 | it('will have google_auth method', () => {
46 | expect(factory.hasMethod('google_auth')).is.eq(true);
47 | });
48 |
49 | it('won\'t have unknown method', () => {
50 | expect(factory.hasMethod('unknown')).is.eq(false);
51 | });
52 |
53 | it('will create email verification service', () => {
54 | expect(factory.create('email')).is.instanceOf(EmailVerificationService);
55 | });
56 |
57 | it('will create google_auth verification service', () => {
58 | expect(factory.create('google_auth')).is.instanceOf(AuthenticatorVerificationService);
59 | });
60 |
61 | it('will throw when trying to create unsupported method', () => {
62 | expect(() => factory.create('random')).to.throw('random not supported');
63 | });
64 | });
65 |
66 | describe('Test BaseVerificationService', () => {
67 | class StubBaseVerificationService extends BaseVerificationService { }
68 | let storage = new SimpleInMemoryStorageService();
69 | let instance = new StubBaseVerificationService('stub', storage);
70 |
71 | it('will initiate a verification process', async() => {
72 | let result = await instance.initiate(createInitiateParams(), tenantData);
73 |
74 | expect(result).to.have.property('verificationId');
75 | expect(result.code).is.equals('123');
76 | expect(result.expiredOn).is.greaterThan(~~((+new Date()) / 1000));
77 | });
78 |
79 | it('will fail initiation of verification process if expiredOn is invalid', async() => {
80 | const params = createInitiateParams({
81 | policy: { expiredOn: null }
82 | });
83 |
84 | expect(instance.initiate(params, tenantData)).to.be.rejectedWith(InvalidParametersException);
85 | });
86 |
87 | it('will initiate of verification process if forcedVerificationId is set up', async() => {
88 | const forcedId = '886aa661-1fdb-4f29-b8e1-85ab6fc48912';
89 | const params = createInitiateParams({
90 | policy: { forcedVerificationId: forcedId }
91 | });
92 |
93 | expect((await instance.initiate(params, tenantData)).verificationId).is.equals(forcedId);
94 | });
95 |
96 | it('will initiate of verification process and return a new generated digital code', async() => {
97 | const params = {
98 | consumer: 'test@test.com',
99 | generateCode: {
100 | length: 16,
101 | symbolSet: ['DIGITS']
102 | },
103 | policy: {
104 | expiredOn: '01:00:00'
105 | }
106 | };
107 |
108 | const code = (await instance.initiate(params, tenantData)).code;
109 | expect(/^[\d]{16}$/.test(code)).is.eq(true);
110 | });
111 |
112 | it('will fail validation if verificationId doesn\'t existing', async() => {
113 | expect(instance.validate('zzz', {code: '123'}, tenantData)).to.be.rejectedWith(NotFoundException);
114 | });
115 |
116 | it('will fail validation if code is incorrect', async() => {
117 | const forcedId = '886aa661-1fdb-4f29-b8e1-85ab6fc48932';
118 | const initiateParams = createInitiateParams({
119 | policy: { forcedVerificationId: forcedId }
120 | });
121 |
122 | const verificationId = (await instance.initiate(initiateParams, tenantData)).verificationId;
123 | expect(verificationId).is.equals(forcedId);
124 | const isValid = (await instance.validate(forcedId, {code: '321'}, tenantData)).isValid;
125 | expect(isValid).is.eq(false);
126 | });
127 |
128 | it('will successfully validate if all is correct', async() => {
129 | const forcedId = '886aa661-1fdb-4f29-b8e1-85ab6fc48932';
130 | const initiateParams = createInitiateParams({
131 | policy: { forcedVerificationId: forcedId }
132 | });
133 |
134 | expect((await instance.initiate(initiateParams, tenantData)).verificationId).is.equals(forcedId);
135 |
136 | const isValid = (await instance.validate(forcedId, {code: '123'}, tenantData)).isValid;
137 | expect(isValid).is.eq(true);
138 | });
139 |
140 | });
141 |
142 | describe('Test EmailVerificationService', () => {
143 | let factory = container.resolve(VerificationServiceFactoryRegister);
144 | let instance: VerificationService = factory.create('email');
145 |
146 | it('will initiate and send an email', async() => {
147 | const forcedId = '886aa661-1fdb-4f29-b8e1-85ab6fc48932';
148 | const initiateParams = createInitiateParams({
149 | consumer: 'test@test.com',
150 | template: {
151 | 'body': '[{{{CODE}}}]'
152 | },
153 | policy: { forcedVerificationId: forcedId }
154 | });
155 |
156 | expect(await instance.initiate(initiateParams, tenantData)).is.property('verificationId').equals(forcedId);
157 | });
158 |
159 | });
160 |
161 | });
162 |
--------------------------------------------------------------------------------
/src/services/storages.ts:
--------------------------------------------------------------------------------
1 | import { injectable } from 'inversify';
2 | import * as redis from 'redis';
3 | import { RedisClient } from 'redis';
4 | import 'reflect-metadata';
5 |
6 | import config from '../config';
7 |
8 | // Exceptions
9 |
10 | export class StorageException extends Error { }
11 |
12 | // Types
13 |
14 | export const StorageServiceType = Symbol('StorageServiceType');
15 |
16 | /**
17 | * Very simple memory storage implementation.
18 | */
19 | @injectable()
20 | export class SimpleInMemoryStorageService implements StorageService {
21 |
22 | private storage: {
23 | [key: string]: {
24 | ttl?: number;
25 | data: any;
26 | };
27 | } = {};
28 | private gcMinimalTimeInterval: number = 10;
29 | private lastGcTime: number = this.getNextGcTime();
30 |
31 | /**
32 | * @inheritdoc
33 | */
34 | public set(name: string, value: T, options?: ValueOptions): Promise {
35 | this.gc();
36 | if (options) {
37 | this.storage[name] = {
38 | ttl: options.ttlInSeconds * 1000 + +new Date(),
39 | data: JSON.stringify(value)
40 | };
41 | } else {
42 | this.storage[name] = {
43 | data: JSON.stringify(value)
44 | };
45 | }
46 | return Promise.resolve(value);
47 | }
48 |
49 | /**
50 | * @inheritdoc
51 | */
52 | public remove(name: string): Promise {
53 | const value = typeof this.storage[name] === 'undefined' ? null : JSON.parse(this.storage[name].data);
54 | delete this.storage[name];
55 | return Promise.resolve(value);
56 | }
57 |
58 | /**
59 | * @inheritdoc
60 | */
61 | public get(name: string, defaultValue: T): Promise {
62 | this.gc();
63 | return Promise.resolve(typeof this.storage[name] === 'undefined' ? defaultValue : JSON.parse(this.storage[name].data));
64 | }
65 |
66 | /**
67 | * Execute simple garbage collection
68 | */
69 | public gc() {
70 | /* istanbul ignore if */
71 | if (this.lastGcTime < +new Date()) {
72 | Object.keys(this.storage).forEach((key) => {
73 | if (this.storage[key].ttl && this.storage[key].ttl < +new Date()) {
74 | delete this.storage[key];
75 | }
76 | });
77 | this.lastGcTime = this.getNextGcTime();
78 | }
79 | }
80 |
81 | private getNextGcTime(): number {
82 | return this.gcMinimalTimeInterval * 1000 + (+new Date());
83 | }
84 |
85 | }
86 |
87 | const redisConfig = config.redis;
88 |
89 | /**
90 | * Redis storage implementation.
91 | */
92 | @injectable()
93 | export class RedisStorageService implements StorageService {
94 |
95 | private client: RedisClient;
96 |
97 | constructor() {
98 | this.client = this.createRedisClient();
99 | }
100 |
101 | protected createRedisClient(): RedisClient {
102 | return redis.createClient(
103 | redisConfig.url
104 | );
105 | }
106 |
107 | /**
108 | * @inheritdoc
109 | */
110 | public set(name: string, value: T, options?: ValueOptions): Promise {
111 | if (options) {
112 | return new Promise((resolve, reject) => {
113 | this.client.setex(this.getKey(name),
114 | options.ttlInSeconds,
115 | JSON.stringify(value),
116 | (err: any, result) => {
117 | if (err) {
118 | return reject(new StorageException(err));
119 | }
120 | resolve(value);
121 | }
122 | );
123 | });
124 | } else {
125 | return new Promise((resolve, reject) => {
126 | this.client.set(this.getKey(name),
127 | JSON.stringify(value),
128 | (err: any, result) => {
129 | if (err) {
130 | return reject(new StorageException(err));
131 | }
132 | resolve(value);
133 | }
134 | );
135 | });
136 | }
137 | }
138 |
139 | /**
140 | * @inheritdoc
141 | */
142 | public remove(name: string): Promise {
143 | return new Promise((resolve, reject) => {
144 | this.client.del(this.getKey(name),
145 | (err: any, result) => {
146 | if (err) {
147 | return reject(new StorageException(err));
148 | }
149 | try {
150 | resolve(JSON.parse(result));
151 | } catch (error) {
152 | reject(new StorageException(error));
153 | }
154 | }
155 | );
156 | });
157 | }
158 |
159 | /**
160 | * @inheritdoc
161 | */
162 | public get(name: string, defaultValue: T): Promise {
163 | return new Promise((resolve, reject) => {
164 | this.client.get(this.getKey(name),
165 | (err: any, result) => {
166 | if (err) {
167 | return reject(new StorageException(err));
168 | }
169 | try {
170 | if (!result) {
171 | return resolve(defaultValue);
172 | }
173 | resolve(JSON.parse(result));
174 | } catch (error) {
175 | reject(new StorageException(error));
176 | }
177 | }
178 | );
179 | });
180 | }
181 |
182 | private getKey(key: string): string {
183 | return redisConfig.prefix + key;
184 | }
185 |
186 | }
187 |
--------------------------------------------------------------------------------
/src/services/verifications.ts:
--------------------------------------------------------------------------------
1 | import { injectable, inject } from 'inversify';
2 | import 'reflect-metadata';
3 |
4 | import AuthenticatorVerificationService from './authenticator.verification';
5 | import EmailVerificationService from './email.verification';
6 | import { StorageServiceType } from './storages';
7 | import { EmailProviderService, EmailProviderServiceType } from './providers/index';
8 | import { InvalidParametersException } from '../exceptions/exceptions';
9 |
10 | export const VerificationServiceFactoryType = Symbol('VerificationServiceFactoryType');
11 |
12 | /**
13 | * VerificationServiceFactory interface.
14 | */
15 | export interface VerificationServiceFactory {
16 | create(method: string): VerificationService;
17 | hasMethod(method: string): boolean;
18 | }
19 |
20 | const EMAIL_VERIFICATION_METHOD = 'email';
21 | const AUTHENTICATOR_VERIFICATION_METHOD = 'google_auth';
22 |
23 | /**
24 | * VerificationServiceFactory implementation.
25 | */
26 | @injectable()
27 | export class VerificationServiceFactoryRegister implements VerificationServiceFactory {
28 | constructor(
29 | @inject(StorageServiceType) private storageService: StorageService,
30 | @inject(EmailProviderServiceType) private emailProviderService: EmailProviderService
31 | ) { }
32 |
33 | /**
34 | * Create concrete verificator service
35 | *
36 | * @param method
37 | */
38 | create(method: string): VerificationService {
39 | switch (method) {
40 | case EMAIL_VERIFICATION_METHOD:
41 | return new EmailVerificationService(method, this.storageService, this.emailProviderService);
42 | case AUTHENTICATOR_VERIFICATION_METHOD:
43 | return new AuthenticatorVerificationService(method, this.storageService);
44 | default:
45 | throw new InvalidParametersException(`${method} not supported`);
46 | }
47 | }
48 |
49 | hasMethod(method: string): boolean {
50 | const allowedMethods = [
51 | EMAIL_VERIFICATION_METHOD,
52 | AUTHENTICATOR_VERIFICATION_METHOD
53 | ];
54 |
55 | return allowedMethods.indexOf(method) !== -1;
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "buildOnSave": false,
4 | "compilerOptions": {
5 | "module": "commonjs",
6 | "target": "es6",
7 | "noImplicitAny": false,
8 | "sourceMap": false,
9 | "lib": ["es2015", "dom"],
10 | "outDir": "dist",
11 | "experimentalDecorators": true,
12 | "emitDecoratorMetadata": true
13 | },
14 | "exclude": [
15 | "node_modules",
16 | "**/*.spec.ts",
17 | "dist"
18 | ]
19 | }
20 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "buildOnSave": false,
4 | "compilerOptions": {
5 | "module": "commonjs",
6 | "target": "es6",
7 | "noImplicitAny": false,
8 | "sourceMap": false,
9 | "lib": ["es2015", "dom"],
10 | "outDir": "dist",
11 | "experimentalDecorators": true,
12 | "emitDecoratorMetadata": true
13 | },
14 | "exclude": [
15 | "node_modules",
16 | "dist"
17 | ]
18 | }
19 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["tslint-config-standard"],
3 | "rules": {
4 | "no-unused-variable": true,
5 | "quotemark": [true, "single", "jsx-double"],
6 | "semicolon": [true, "always"],
7 | "space-before-function-paren": [true, "never"],
8 | "member-ordering": [false],
9 | "handle-callback-err": [false]
10 | }
11 | }
12 |
--------------------------------------------------------------------------------