Compare commits

...

120 Commits

Author SHA1 Message Date
a41882de60 update
All checks were successful
continuous-integration/drone/push Build is passing
2023-02-09 11:58:10 +01:00
ba9608829d permissions, connection data reader
All checks were successful
continuous-integration/drone/push Build is passing
2022-09-12 12:30:06 +02:00
910099285b fix readme
Some checks failed
continuous-integration/drone/push Build is failing
2022-09-09 16:38:49 +02:00
6856ac718f fix
All checks were successful
continuous-integration/drone/push Build is passing
2022-09-09 16:02:25 +02:00
6eb2009141 fix
Some checks failed
continuous-integration/drone/push Build is failing
2022-09-09 15:53:38 +02:00
64d4f00629 blacklist with automatic garbage collector
Some checks failed
continuous-integration/drone/push Build is failing
2022-09-09 15:49:53 +02:00
31f739d4b8 blacklist sync
All checks were successful
continuous-integration/drone/push Build is passing
2022-08-27 16:39:07 +02:00
e80e3f9a94 improve debug, redis storage structure
Some checks failed
continuous-integration/drone/push Build is failing
2022-08-15 17:33:25 +02:00
b7514941f0 Revert "refactoring redis for multiple value classes"
This reverts commit d5c136790e.
2022-08-15 13:56:02 +02:00
d5c136790e refactoring redis for multiple value classes
Some checks failed
continuous-integration/drone/push Build is failing
2022-08-13 17:18:09 +02:00
43cf782511 fix
All checks were successful
continuous-integration/drone/push Build is passing
2022-08-10 16:18:05 +02:00
b43190d048 fix redis sync
Some checks failed
continuous-integration/drone/push Build is failing
2022-08-10 16:17:00 +02:00
7bb6dac737 fix
All checks were successful
continuous-integration/drone/push Build is passing
2022-08-10 11:11:39 +02:00
1009a9b8d5 catch key error
Some checks failed
continuous-integration/drone/push Build is failing
2022-08-10 11:08:14 +02:00
da36f87250 fix
All checks were successful
continuous-integration/drone/push Build is passing
2022-08-09 08:53:17 +02:00
cf2f9c0182 start redis server
All checks were successful
continuous-integration/drone/push Build is passing
2022-08-08 16:10:33 +02:00
4d69efd9f5 debug
Some checks failed
continuous-integration/drone/push Build is failing
2022-08-08 15:59:53 +02:00
fd26975559 redis sync
Some checks failed
continuous-integration/drone/push Build is failing
2022-08-08 15:52:56 +02:00
122bd7b574 fix refresh data carrying
All checks were successful
continuous-integration/drone/push Build is passing
2022-08-03 16:21:00 +02:00
84be087743 logout function
All checks were successful
continuous-integration/drone/push Build is passing
2022-05-02 13:30:30 +02:00
ec08f8f04e option to enable body parsing
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-26 09:08:45 +01:00
cc8762e4ec cookie settings
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-10 10:06:54 +01:00
3aaaf10fd9 improved cookie security
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-08 22:10:21 +01:00
8f047f2700 fixed wrong return type
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-05 13:01:32 +01:00
80a98704af fix
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-05 12:58:00 +01:00
c7708f4bc0 fix
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-05 12:35:18 +01:00
b58af27719 add debug logging
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-05 12:32:04 +01:00
2a51e0a753 fix refresh cookie settings
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-05 08:14:35 +01:00
22075489c2 automatic refresh tokens
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-04 21:32:04 +01:00
1188e4573f fix promise not being awaited
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-04 15:01:33 +01:00
d28be9e3f8 fix unreliable 'successful' flag, don't set content-type on leave_open
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-04 13:39:10 +01:00
dab45e39a6 flag to leave request open on auth
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-03 16:26:45 +01:00
4820bda8ca get boolean return from auth handler
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-03 15:40:13 +01:00
86b07af63d fix
All checks were successful
continuous-integration/drone/push Build is passing
2022-01-03 14:46:12 +01:00
85a5f3c2fb allow gateway without redirection, manual request handling
Some checks failed
continuous-integration/drone/push Build is failing
2022-01-03 14:44:27 +01:00
c55ed33e53 fix line endings
Some checks failed
continuous-integration/drone/push Build is failing
2021-05-24 14:43:14 +02:00
3bc5538a69 formatting
All checks were successful
continuous-integration/drone/push Build is passing
2021-05-10 12:41:00 +02:00
e7ad5656e3 allow immediate redirect on auth
Some checks failed
continuous-integration/drone/push Build is failing
2021-05-10 12:26:56 +02:00
a3f021fdd2 clearing instances
All checks were successful
continuous-integration/drone/push Build is passing
2021-01-15 14:45:18 +01:00
d286548850 fix
All checks were successful
continuous-integration/drone/push Build is passing
2021-01-15 13:48:29 +01:00
e326c6c077 publish
Some checks failed
continuous-integration/drone/push Build is failing
2021-01-15 13:45:03 +01:00
ce58c0d204 simplify export data
Some checks failed
continuous-integration/drone/push Build is failing
2021-01-14 21:31:21 +01:00
9ec97d8aa2 fix
Some checks failed
continuous-integration/drone/push Build is failing
2021-01-12 22:04:24 +01:00
1af8c0702c test with additional cookies 2021-01-12 21:26:19 +01:00
e6039e78b1 allow autorization via body only
Some checks failed
continuous-integration/drone/push Build is failing
2021-01-12 21:19:56 +01:00
c5bc0855d7 blacklist import export
Some checks failed
continuous-integration/drone/push Build is failing
2021-01-09 12:20:14 +01:00
d6a40871c4 export/import keys
Some checks failed
continuous-integration/drone/push Build is failing
2021-01-08 13:30:53 +01:00
4c42a682d5 fixes
Some checks failed
continuous-integration/drone/push Build is failing
2021-01-07 15:43:54 +01:00
fd4f891b3e asymmetric keys import/export
Some checks failed
continuous-integration/drone/push Build is failing
2021-01-06 22:43:03 +01:00
adfeeaa52c asymmetric keys
Some checks failed
continuous-integration/drone/push Build is failing
2021-01-06 16:06:03 +01:00
1437316519 fix packaging
Some checks failed
continuous-integration/drone/push Build is failing
2021-01-06 12:17:04 +01:00
5df2577e71 allow bearer and other types of authorization in default handler
Some checks failed
continuous-integration/drone/push Build is failing
2021-01-06 11:38:56 +01:00
df8de9e0c8 fix
Some checks failed
continuous-integration/drone/push Build is failing
2021-01-06 11:15:56 +01:00
05f2e53a8f fix
Some checks failed
continuous-integration/drone/push Build is failing
2021-01-05 22:10:41 +01:00
8285e58337 fix
Some checks failed
continuous-integration/drone/push Build is failing
2021-01-05 21:35:45 +01:00
6cf6286fb8 package
Some checks failed
continuous-integration/drone/push Build is failing
2021-01-05 17:06:35 +01:00
872661a926 full documentation
Some checks failed
continuous-integration/drone/push Build is failing
2021-01-05 16:50:23 +01:00
48afa73ae8 fix
Some checks failed
continuous-integration/drone/push Build is failing
2021-01-05 15:59:06 +01:00
debb7debf1 allow attaching of custom data
Some checks failed
continuous-integration/drone/push Build is failing
2021-01-03 15:32:29 +01:00
80d04f7441 allow signed data storage
Some checks failed
continuous-integration/drone/push Build is failing
2021-01-03 15:13:03 +01:00
f39759bad9 complete redesign 2021-01-03 14:51:22 +01:00
4c27d0eace auth handler tests
Some checks failed
continuous-integration/drone/push Build is failing
2021-01-01 14:14:19 +01:00
83a402db8b auth handler
Some checks failed
continuous-integration/drone/push Build is failing
2020-12-30 19:39:49 +01:00
2f342b31f7 update
Some checks failed
continuous-integration/drone/push Build is failing
2020-12-30 17:21:56 +01:00
a8fb92b367 testing gateway
Some checks failed
continuous-integration/drone/push Build is failing
2020-12-28 16:53:08 +01:00
051c2bdbbd fix copy notices
Some checks failed
continuous-integration/drone/push Build is failing
2020-12-28 15:04:52 +01:00
b27ab8c6fc more tests, stryker
Some checks failed
continuous-integration/drone/push Build is failing
2020-12-28 14:53:14 +01:00
669bc19943 tests for authority
Some checks failed
continuous-integration/drone/push Build is failing
2020-12-19 16:19:09 +01:00
8a264bfa58 separate authority
Some checks failed
continuous-integration/drone/push Build is failing
2020-12-19 15:40:49 +01:00
170eb8a743 improve signature structure, more tests
Some checks failed
continuous-integration/drone/push Build is failing
2020-12-13 13:37:11 +01:00
68c06b6742 cookie auth
Some checks failed
continuous-integration/drone/push Build is failing
2020-12-13 12:26:40 +01:00
a4892f6262 blacklist, gateway
Some checks failed
continuous-integration/drone/push Build is failing
2020-12-12 15:53:47 +01:00
ddde2806d8 test edge case
Some checks failed
continuous-integration/drone/push Build is failing
2020-12-06 21:29:11 +01:00
0be180f632 key store
Some checks failed
continuous-integration/drone/push Build is failing
2020-12-06 21:08:42 +01:00
008fd3f545 starting gateway
Some checks failed
continuous-integration/drone/push Build is failing
2020-12-06 15:51:59 +01:00
01cb121a68 lint
Some checks failed
continuous-integration/drone/push Build is failing
2020-12-03 10:07:49 +01:00
210696dda0 redesign
Some checks failed
continuous-integration/drone/push Build is failing
2020-12-03 09:54:27 +01:00
fe037d43d3 fix
Some checks failed
continuous-integration/drone/push Build is failing
2020-11-02 19:14:30 +01:00
e9c111ff55 update stryker
All checks were successful
continuous-integration/drone/push Build is passing
2020-10-12 18:02:10 +02:00
9ea4f4664c lint
Some checks failed
continuous-integration/drone/push Build is failing
2020-10-05 07:06:26 +02:00
2ee7a11fea fix stryker config
Some checks failed
continuous-integration/drone/push Build is failing
2020-10-04 12:31:01 +02:00
c6bd55eb0d use jasmine
All checks were successful
continuous-integration/drone/push Build is passing
2020-09-28 11:46:30 +02:00
055bb84a70 update-scanner: automatic update
Some checks failed
continuous-integration/drone/push Build is failing
eslint: 7.7.0 ==> 7.8.1 minor
node-fetch: 2.6.0 ==> 2.6.1 minor
2020-09-07 13:41:36 +02:00
cd9d0aff5b update-scanner: automatic update
Some checks failed
continuous-integration/drone/push Build is failing
ava: 3.11.1 ==> 3.12.1 minor
2020-08-30 16:04:24 +02:00
abf98cc4ad update-scanner: automatic update
Some checks failed
continuous-integration/drone/push Build is failing
@sapphirecode/password-helper: 1.0.47 ==> 1.0.48 minor
eslint: 7.6.0 ==> 7.7.0 minor
2020-08-19 08:32:53 +02:00
5b9dc10db9 fix drone config
Some checks failed
continuous-integration/drone/push Build is failing
2020-08-07 08:11:17 +02:00
c425882e20 update-scanner: automatic update
Some checks failed
continuous-integration/drone/push Build is failing
ava: 3.10.1 ==> 3.11.1 minor
eslint: 7.5.0 ==> 7.6.0 minor
2020-08-04 13:04:38 +02:00
a77180d637 update-scanner: automatic update
All checks were successful
continuous-integration/drone/push Build is passing
@sapphirecode/auth-client-helper: 1.0.56 ==> 1.1.1 minor
2020-07-25 16:56:30 +02:00
fe0b409db5 update-scanner: automatic update
All checks were successful
continuous-integration/drone/push Build is passing
@sapphirecode/consts: 1.1.27 ==> 1.1.28 minor
@sapphirecode/crypto-helper: 1.1.55 ==> 1.1.57 minor
@sapphirecode/eslint-config: 2.1.15 ==> 2.1.16 minor
@sapphirecode/password-helper: 1.0.46 ==> 1.0.47 minor
2020-07-19 15:15:31 +02:00
a4a13e665a fix
Some checks failed
continuous-integration/drone/push Build is failing
2020-07-19 13:56:06 +02:00
b116d4e164 update-scanner: automatic update
Some checks failed
continuous-integration/drone/push Build is failing
eslint: 7.4.0 ==> 7.5.0 minor
2020-07-19 12:19:37 +02:00
064ddd0a1e testing possible bug
Some checks failed
continuous-integration/drone/push Build is failing
2020-07-12 19:41:59 +02:00
cf927114c2 fix
All checks were successful
continuous-integration/drone/push Build is passing
2020-07-10 19:30:53 +02:00
c40e6c19ea trying to fix test
Some checks failed
continuous-integration/drone/push Build is failing
2020-07-10 16:21:39 +02:00
507c0ceba3 user_id in connection info
Some checks failed
continuous-integration/drone/push Build is failing
2020-07-10 15:39:14 +02:00
8f131a932f update-scanner: automatic update
All checks were successful
continuous-integration/drone/push Build is passing
@sapphirecode/auth-client-helper: 1.0.54 ==> 1.0.56 minor
@sapphirecode/consts: 1.1.25 ==> 1.1.27 minor
@sapphirecode/crypto-helper: 1.1.53 ==> 1.1.55 minor
@sapphirecode/eslint-config: 2.1.13 ==> 2.1.15 minor
@sapphirecode/password-helper: 1.0.44 ==> 1.0.46 minor
@stryker-mutator/core: 3.3.0 ==> 3.3.1 minor
@stryker-mutator/javascript-mutator: 3.3.0 ==> 3.3.1 minor
ava: 3.9.0 ==> 3.10.1 minor
eslint: 7.3.1 ==> 7.4.0 minor
2020-07-10 12:41:03 +02:00
1d944287f1 switch to drone
All checks were successful
continuous-integration/drone/push Build is passing
2020-07-10 08:25:39 +02:00
26261da0a5 update-scanner: automatic update
@sapphirecode/auth-client-helper: 1.0.53 ==> 1.0.54 minor
@sapphirecode/consts: 1.1.24 ==> 1.1.25 minor
@sapphirecode/crypto-helper: 1.1.52 ==> 1.1.53 minor
@sapphirecode/eslint-config: 2.1.12 ==> 2.1.13 minor
@sapphirecode/password-helper: 1.0.43 ==> 1.0.44 minor
2020-07-01 09:39:49 +02:00
d22776f7b9 update-scanner: automatic update
@sapphirecode/auth-client-helper: 1.0.52 ==> 1.0.53 minor
@sapphirecode/consts: 1.1.23 ==> 1.1.24 minor
@sapphirecode/crypto-helper: 1.1.51 ==> 1.1.52 minor
@sapphirecode/eslint-config: 2.1.10 ==> 2.1.12 minor
@sapphirecode/password-helper: 1.0.42 ==> 1.0.43 minor
eslint: 7.3.0 ==> 7.3.1 minor
2020-06-24 12:58:56 +02:00
573ca9e633 update-scanner: automatic update
@sapphirecode/auth-client-helper: 1.0.51 ==> 1.0.52 minor
@sapphirecode/consts: 1.1.22 ==> 1.1.23 minor
@sapphirecode/crypto-helper: 1.1.50 ==> 1.1.51 minor
@sapphirecode/eslint-config: 2.1.9 ==> 2.1.10 minor
@sapphirecode/password-helper: 1.0.41 ==> 1.0.42 minor
eslint: 7.2.0 ==> 7.3.0 minor
2020-06-22 08:29:17 +02:00
f9f93fb5bc update-scanner: automatic update
@sapphirecode/auth-client-helper: 1.0.50 ==> 1.0.51 minor
@sapphirecode/crypto-helper: 1.1.49 ==> 1.1.50 minor
@sapphirecode/password-helper: 1.0.40 ==> 1.0.41 minor
@stryker-mutator/core: 3.2.4 ==> 3.3.0 minor
@stryker-mutator/javascript-mutator: 3.2.4 ==> 3.3.0 minor
ava: 3.8.2 ==> 3.9.0 minor
2020-06-19 13:16:55 +02:00
53c2deefc9 update-scanner: automatic update
@sapphirecode/auth-client-helper: 1.0.49 ==> 1.0.50 minor
@sapphirecode/consts: 1.1.21 ==> 1.1.22 minor
@sapphirecode/crypto-helper: 1.1.48 ==> 1.1.49 minor
@sapphirecode/eslint-config: 2.1.8 ==> 2.1.9 minor
@sapphirecode/password-helper: 1.0.39 ==> 1.0.40 minor
eslint: 7.1.0 ==> 7.2.0 minor
2020-06-11 20:39:08 +02:00
7724ccc7bf update-scanner: automatic update
@sapphirecode/auth-client-helper: 1.0.48 ==> 1.0.49 minor
@sapphirecode/consts: 1.1.20 ==> 1.1.21 minor
@sapphirecode/crypto-helper: 1.1.47 ==> 1.1.48 minor
@sapphirecode/eslint-config: 2.1.7 ==> 2.1.8 minor
@sapphirecode/password-helper: 1.0.38 ==> 1.0.39 minor
nyc: 15.0.1 ==> 15.1.0 minor
2020-06-02 08:56:08 +02:00
3006a0b918 update-scanner: automatic update
@sapphirecode/auth-client-helper: 1.0.47 ==> 1.0.48 minor
@sapphirecode/consts: 1.1.19 ==> 1.1.20 minor
@sapphirecode/crypto-helper: 1.1.46 ==> 1.1.47 minor
@sapphirecode/eslint-config: 2.1.6 ==> 2.1.7 minor
@sapphirecode/password-helper: 1.0.37 ==> 1.0.38 minor
@stryker-mutator/core: 3.2.3 ==> 3.2.4 minor
@stryker-mutator/javascript-mutator: 3.2.3 ==> 3.2.4 minor
eslint: 7.0.0 ==> 7.1.0 minor
2020-05-23 18:30:42 +02:00
6fc9895884 update-scanner: automatic update
@sapphirecode/auth-client-helper: 1.0.45 ==> 1.0.47 minor
@sapphirecode/consts: 1.1.18 ==> 1.1.19 minor
@sapphirecode/crypto-helper: 1.1.44 ==> 1.1.46 minor
@sapphirecode/eslint-config: 2.1.4 ==> 2.1.6 minor
@sapphirecode/password-helper: 1.0.35 ==> 1.0.37 minor
2020-05-17 19:47:30 +02:00
4168ba8cec fix 2020-05-17 18:55:29 +02:00
e10d665c2b update 2020-05-17 17:37:41 +02:00
9e8fb8fc60 update jenkins.js 2020-05-15 13:16:03 +02:00
560f558e0d adapt jenkins.js 2020-05-13 16:05:26 +02:00
cdc37e93b3 update-scanner: automatic update
@sapphirecode/auth-client-helper: 1.0.43 ==> 1.0.44 minor
@sapphirecode/crypto-helper: 1.1.42 ==> 1.1.43 minor
@sapphirecode/password-helper: 1.0.33 ==> 1.0.34 minor
ava: 3.8.1 ==> 3.8.2 minor
eslint: 6.8.0 ==> 7.0.0 major
2020-05-09 22:01:23 +02:00
5a6a74c614 update-scanner: automatic update
@sapphirecode/auth-client-helper: 1.0.42 ==> 1.0.43 minor
@sapphirecode/consts: 1.1.15 ==> 1.1.16 minor
@sapphirecode/crypto-helper: 1.1.41 ==> 1.1.42 minor
@sapphirecode/eslint-config: 2.0.24 ==> 2.0.25 minor
@sapphirecode/password-helper: 1.0.32 ==> 1.0.33 minor
2020-05-08 13:51:09 +02:00
605ee9d73b update-scanner: automatic update
@sapphirecode/auth-client-helper: 1.0.41 ==> 1.0.42 minor
@sapphirecode/consts: 1.1.14 ==> 1.1.15 minor
@sapphirecode/crypto-helper: 1.1.40 ==> 1.1.41 minor
@sapphirecode/eslint-config: 2.0.23 ==> 2.0.24 minor
@sapphirecode/password-helper: 1.0.31 ==> 1.0.32 minor
2020-05-07 10:41:09 +02:00
af465b47d9 update-scanner: automatic update
@sapphirecode/auth-client-helper: 1.0.39 ==> 1.0.41 minor
@sapphirecode/crypto-helper: 1.1.39 ==> 1.1.40 minor
2020-05-06 10:20:31 +02:00
3a6910f406 fix 2020-05-06 10:09:26 +02:00
18c0909276 update-scanner: automatic update
@sapphirecode/consts: 1.1.13 ==> 1.1.14 minor
@sapphirecode/password-helper: 1.0.30 ==> 1.0.31 minor
2020-05-06 10:07:38 +02:00
9e4d6477b7 fix 2020-05-06 08:40:58 +02:00
7e2c3742af fix 2020-05-06 07:50:42 +02:00
640d8d6889 fix 2020-05-06 07:44:04 +02:00
6c6c0bc35d fix 2020-05-06 07:32:50 +02:00
552947bf2e fix yarn.lock 2020-05-05 19:52:00 +02:00
43 changed files with 6830 additions and 7556 deletions

15
.drone.yml Normal file
View File

@ -0,0 +1,15 @@
kind: pipeline
name: default
steps:
- name: setup
image: registry:5000/node-build
commands:
- yarn
- curl https://git.scode.ovh/Timo/standard/raw/branch/master/ci.js > ci.js
- name: build
image: registry:5000/node-build
commands:
- redis-server --daemonize yes
- node ci.js

2
.eslintignore Normal file
View File

@ -0,0 +1,2 @@
/dist/
*.d.ts

View File

@ -2,23 +2,21 @@
* Copyright (C) Sapphirecode - All Rights Reserved
* This file is part of Auth-Server-Helper which is released under MIT.
* See file 'LICENSE' for full license details.
* Created by Timo Hocker <timo@scode.ovh>, March 2020
* Created by Timo Hocker <timo@scode.ovh>, December 2020
*/
'use strict';
module.exports = {
env: {
commonjs: true,
es6: true,
node: true
es6: true,
node: true
},
extends: [
'@scode'
],
extends: [ '@sapphirecode' ],
globals: {
Atomics: 'readonly',
Atomics: 'readonly',
SharedArrayBuffer: 'readonly'
},
parserOptions: {
ecmaVersion: 2018
}
}
parserOptions: { ecmaVersion: 2018 }
};

4
.gitignore vendored
View File

@ -2,6 +2,4 @@
/dist/
/.nyc_output/
/coverage/
/db.sqlite
# stryker temp files
.stryker-tmp
/.stryker-tmp/

8
.liconfig.json Normal file
View File

@ -0,0 +1,8 @@
{
"has_license": true,
"license": "MIT",
"author": "Timo Hocker",
"company": "Sapphirecode",
"email": "timo@scode.ovh",
"software": "Auth-Server-Helper"
}

54
CHANGELOG.md Normal file
View File

@ -0,0 +1,54 @@
# Changelog
## 4.1.0
- Permission Management
- Gateway function to read connection info
## 4.0.0
- Blacklist entries can now be synchronized through redis
BREAKING: Blacklist functions are now asynchronous
## 3.3.0
- Verification Keys can now be synchronized through redis
## 3.2.0
- Logout function
## 3.1.0
- Option to enable body parsing
## 3.0.0
- Allows Cookies Parameters to be set
BREAKING:
- All cookie_name and refresh_cookie_name properties have been renamed to cookie and refresh_cookie and are now a settings object instead of a string
## 2.2.0
- Allow refresh tokens to be sent on a separate cookie
- Automatic token refresh if the access token is expired and the cookie header contains a valid refresh token
## 2.1.0
- Allow access to Gateway functions like authenticate, get_cookie_auth, get_header_auth, redirect, deny
- Allow Gateway to deny a request in case no redirect url is specified
## 2.0.0
Complete redesign
## 1.1.0
add user_id to res.connection, so request handlers can access the current user
## 1.0.0
initial release

23
Jenkinsfile vendored
View File

@ -1,23 +0,0 @@
pipeline {
agent any
environment {
VERSION = VersionNumber([
versionNumberString:
'${BUILDS_ALL_TIME}',
versionPrefix: '1.0.',
worstResultForIncrement: 'SUCCESS'
])
}
stages {
stage('Building') {
steps {
script {
currentBuild.displayName = env.VERSION
}
sh 'yarn ci ${VERSION}'
}
}
}
}

7
LICENSE Normal file
View File

@ -0,0 +1,7 @@
MIT License Copyright (c) <year> <author>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice (including the next paragraph) shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

304
README.md
View File

@ -1,42 +1,290 @@
# Auth Server Helper
# auth-server-helper
Authentication middleware for express
version: 4.1.x
customizable and simple authentication
## Installation
npm:
> npm i --save auth-server-helper
yarn:
> yarn add auth-server-helper
## Usage
### 1. put a gateway in front of the routes you want to secure
```js
const auth = require('@scode/auth-server-helper');
const password_helper = require('@scode/password_helper');
const {create_gateway} = require('@sapphirecode/auth-server-helper');
const users = {
foo: {
id: 0
password: await password_helper.hash('bar'),
salt: '123'
const gateway = create_gateway({
redirect_url: '/auth', // if defined, unauthorized requests will be redirected
cookie: { name: 'auth_cookie' }, // if defined, access tokens will be read from or written to this cookie,
refresh_cookie: { name: 'refresh_cookie' }, // if defined, refresh tokens will be read and used to automatically refresh client tokens (requires the refresh_settings attribute)
refresh_settings: {
// same as settings for allow_access under section 2
// the options data, redirect_to and leave_open are not supported here
}
}
});
// add cookieParser to allow session management via cookies
app.use(cookieParser());
// the middleware needs a function to determine user data
// this function can also return a promise
app.use(auth((user_name) => {
if (!users[user_name])
return null;
return users[user_name];
}));
// express
app.use(gateway);
// node http
http.createServer((main_req, main_res) =>
gateway(main_req, main_res, (req, res) => {
// your request handler
});
);
```
when a client logs in, it will set a header called 'session' that the client can use to authorize the following requests.
it also sets a cookie to make requesting from the client more simple. (cookie parser is needed to make authentication with cookies possible)
the gateway will forward any authorized requests to the next handler and
redirect all others to the specified url
## Excluding routes
exceptions to the auth module can be added by adding an array of regular expressions
a specific method can also be filtered for by giving an object instead of a plain regular expression.
#### 1.1. Creating a gateway for manual processing of requests
```js
auth(..., [/no-auth/, {regex: '/no-auth-post/', method: 'POST'}]);
```
const {GatewayClass} = require('@sapphirecode/auth-server-helper');
const gateway = new GatewayClass({ /* options */ }); // options are the same as for create_gateway above
// process a request
if (gateway.authenticate(http_request)) { // returns true if request is valid and sets req.connection.token_id and .token_data
console.log('access granted');
} else {
gateway.redirect(response); // redirects the client, triggers deny if no redirect_url was set in options
// or
gateway.deny(response); // sends status 403
}
```
### 2. creating the auth endpoint
```js
const {create_auth_handler} = require('@sapphirecode/auth-server-helper');
const handler = create_auth_handler(
async (req) => {
if (req.user === 'foo' && req.password === 'bar')
const {access_token_id, refresh_token_id} = await req.allow_access({
access_token_expires_in: 600, // seconds until access tokens expire
include_refresh_token: true, // should the answer include a refresh token? default: false
refresh_token_expires_in: 3600, // seconds until refresh tokens expire (required if refresh tokens are generated)
data: {user: 'foo'}, // additional custom data to include in the token
});
if (req.user === 'part' && req.password === 'baz')
const part_id = await req.allow_part(
60, // seconds until part_token expires
'some_module', // next module handler (defined below)
{foo: 'bar'} // custom data to attach to the token
);
// all allow_ functions return a token id, which can later be used to invalidate specific tokens from the server side
req.deny();
},
{
refresh: {
/*...same options as allow_access */
}, // define the behaviour of refresh tokens. Refresh tokens will not be accepted if this option is undefined
modules: {
some_module(req) {
// request handlers for part_tokens
// access custom data:
const auth_data = req.request.connection.auth;
auth_data.token_id; // token id
auth_data.token_data; // custom data
// the same works in handlers after the gateway, information is always stored in request.connection.auth
},
},
cookie: { name: 'auth_cookie' }, // if defined, access tokens will be stored in this cookie,
refresh_cookie: { name: 'refresh_cookie' }, // if defined, refresh tokens will be stored in this cookie
parse_body: true // read the request body into a string (default false)
}
);
// express
app.use(handler);
// node http
// ... create server, on path /auth run the handler
handler(req, res); // the handler will also return true if allow_access or allow_part was called
```
after the auth handler, the request will be completed, no additional content
should be served here. (Read 2.1 for info on disabling this)
#### 2.1. Processing Auth Requests without closing the response object
to prevent the auth handler from closing the response object you can provide
additional options on each of the allow/deny functions.
```js
allow_access({leave_open: true, ...});
allow_part(
60,
'some_module',
{foo: 'bar'},
true // additional flag to leave request open
);
invalid('error description', true);
deny(true);
```
if this flag is set, no data will be written to the response body and no data
will be sent. Status code and Headers will still be set.
### Defining Custom Cookie Settings
By default all cookies will be sent with 'Secure; HttpOnly; SameSite=Strict'
Attributes
In the appropriate settings object, you can set the following options:
```js
{
name: 'foo', // name of the cookies
secure: true, // option to enable or disable the Secure option default: true
http_only: true, // option to enable or disable HttpOnly default: true
same_site: 'Strict', // SameSite property (Strict, Lax or None) default: 'Strict'. Set this to null to disable
expires: 'Mon, 10 Jan 2022 09:28:00 GMT', // Expiry date of the cookie
max_age: 600, // Maximum age in Seconds
domain: 'example.com', // Domain property
path: '/cookies_here' // Path property
}
```
For Documentation on the different Cookie Attributes see
<https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#creating_cookies>
### Invalidating tokens after they are delivered to the client
```js
const {blacklist} = require('@sapphirecode/auth-server-helper');
await blacklist.add_signature(token_id); // the token id is returned from any function that creates tokens
```
#### Logout function
```js
const {GatewayClass} = require('@sapphirecode/auth-server-helper');
const gateway = new GatewayClass({ /* options */ });
// create a new express route
app.get('logout', (req, res) => {
// call the gateway's logout function
gateway.logout(req);
// respond ok
res.status(200);
res.end();
});
```
### Exporting and importing public keys to validate tokens across server instances
```js
const {keystore} = require('@sapphirecode/auth-server-helper');
const export = keystore.export_verification_data();
// second instance
keystore.import_verification_data(export);
```
These keys can also be live synchronized with redis to allow sessions to be
shared between servers
```js
const {keystore} = require('@sapphirecode/auth-server-helper');
keystore.sync_redis('redis://localhost');
```
### Exporting and importing blacklist entries across server instances
```js
const {blacklist} = require('@sapphirecode/auth-server-helper');
const export = blacklist.export_blacklist();
// second instance
blacklist.import_blacklist(export);
```
### Clearing Keystore and Blacklist
Resetting the Keystore instance generates a new instance id and deletes all
imported or generated keys.
```js
const {keystore, blacklist} = require('@sapphirecode/auth-server-helper');
// clear keystore
keystore.reset_instance();
// clear blacklist
await blacklist.clear();
// clear blacklist items older than 10 seconds
await blacklist.clear(Date.now() - 10000);
```
### Setting and checking permissions
When allowing access to a client a list of permissions can be added. Permissions
are case sensitive.
```js
allow_access({permissions: ['foo','bar'], ...})
```
The gateway can be told to check those permissions before forwarding a request.
```js
const gateway = new GatewayClass({
require_permissions: ['foo'], // Only clients with the 'foo' permission will be granted access
});
```
additional checks can be run later
```js
(req, res) => {
const has_both = gateway.check_permissions(req, ['foo', 'bar']); // returns true if both permissions are set
const has_bar = gateway.has_permission(req, 'bar'); // returns true if permission 'bar' is set
};
```
### Reading connection info
Data like the used token id, custom data and permissions can be read from
`req.connection.auth` or using the function `gateway.get_info(req)`
```js
const info = gateway.get_info(req);
console.log(info);
/*
{
token_id: 'foo',
data: {}, // custom data
permissions: ['foo','bar']
}
*/
```
## License
MIT © Timo Hocker <timo@scode.ovh>

200
index.js
View File

@ -1,200 +0,0 @@
/*
* Copyright (C) Sapphirecode - All Rights Reserved
* This file is part of Auth-Server-Helper which is released under MIT.
* See file 'LICENSE' for full license details.
* Created by Timo Hocker <timo@scode.ovh>, March 2020
*/
// @ts-nocheck
/* eslint-disable no-magic-numbers */
'use strict';
const password_helper = require ('@scode/password-helper');
const crypto = require ('@scode/crypto-helper');
const consts = require ('@scode/consts');
const me = {};
/**
* initializes the module
*
* @param {Function<Promise|object>} get_user
* function that returns {id:number, password:string, salt:string}
* for a given user identifier
* @param {Array<RegExp>} ignore_paths array of regex to skip auth
* @returns {Function} request handler
*/
function init (get_user, ignore_paths = []) {
me.get_user = get_user;
me.session_timeout_milliseconds = 300000;
me.ignore_paths = ignore_paths;
me.jwt_secret = crypto.create_salt ();
me.app_id = crypto.create_salt ();
return request_handler;
}
/**
* tries to authenticate a user
*
* @param {string} user name or email of the given user
* @param {string} password hashed password
* @returns {Promise<string>} session key if successful
*/
async function authenticate (user, password) {
const user_entry
= await new Promise ((res) => res (me.get_user (user)));
if (!user_entry)
return null;
if (!await password_helper.verify (user_entry.password, password))
return null;
const session_key = crypto.sign_object (
{ id: user_entry.id },
me.jwt_secret
);
return session_key;
}
/**
* gets the correct salt for a given user
*
* @param {string} user user name or email to query
*/
async function salt (user) {
const user_entry
= await new Promise ((res) => res (me.get_user (user)));
if (!user_entry)
return null;
return user_entry.salt;
}
/**
* block if no auth header found
*
* @param {string} session session key
* @param {string} user user name
* @param {any} res response object
* @returns {boolean} true if handler blocked request
*/
function request_handler_block (session, user, res) {
if (typeof session === 'undefined' && typeof user === 'undefined') {
res.status (consts.http.status_unauthorized);
res.end ();
return true;
}
return false;
}
/**
* handle authentication
*
* @param {string} session session key
* @param {string} user user name
* @param {string} key user hash
* @param {any} res response object
* @param {any} next next handler
* @returns {Promise<boolean>} true if handler authenticated
*/
async function request_handler_authenticate (session, user, key, res, next) {
if (typeof session === 'undefined' && typeof user !== 'undefined') {
if (typeof key === 'undefined') {
const user_salt = await salt (user);
res.status (
user_salt === null
? consts.http.status_forbidden
: consts.http.status_ok
);
res.end (user_salt);
return true;
}
const session_key = await authenticate (user, key);
res.status (
session_key === null
? consts.http.status_forbidden
: consts.http.status_ok
)
.cookie (me.app_id, session_key, { maxAge: 900000, httpOnly: true })
.end (session_key);
return true;
}
try {
const jwt = crypto.verify_signature (
session,
me.jwt_secret,
me.session_timeout_milliseconds
);
res.locals.user_id = jwt.id;
const new_user_token = crypto.sign_object (
{ id: jwt.id },
me.jwt_secret
);
res.cookie (
me.app_id,
new_user_token,
{ maxAge: 900000, httpOnly: true }
)
.header ('session', new_user_token);
next ();
return true;
}
catch (err) {
return false;
}
}
/**
* check if a filter matches a request
*
* @param {any} req request
* @param {any} filter filter
* @returns {boolean} true if filter matches
*/
function filter_matches (req, filter) {
if (filter instanceof RegExp && filter.test (req.url))
return true;
return req.method === filter.method
&& filter.regex
&& filter.regex.test (req.url);
}
/**
* handles http requests
*
* @param {any} req request
* @param {any} res response
* @param {any} next next handler
*/
async function request_handler (req, res, next) {
if (Array.isArray (me.ignore_paths)) {
for (const ignore of me.ignore_paths) {
if (filter_matches (req, ignore)) {
next ();
return;
}
}
}
const { user, key, session: header_session } = req.headers;
const cookie_session = typeof req.cookies === 'undefined'
? null
: req.cookies[me.app_id];
const session = cookie_session || header_session;
if (request_handler_block (session, user, res))
return;
if (await request_handler_authenticate (session, user, key, res, next))
return;
res.status (consts.http.status_forbidden);
res.end ();
}
module.exports = init;

12
jasmine.json Normal file
View File

@ -0,0 +1,12 @@
{
"spec_dir": "dist/test",
"spec_files": [
"spec/*.js"
],
"helpers": [
"helpers/*.js"
],
"stopSpecOnExpectationFailure": false,
"random": false
}

View File

@ -1,27 +0,0 @@
/* eslint-disable no-process-exit */
/* eslint-disable no-console */
/* eslint-disable no-sync */
'use strict';
const fs = require ('fs');
const child_process = require ('child_process');
const pkg = JSON.parse (fs.readFileSync ('package.json', 'utf-8'));
[
,, pkg.version
] = process.argv;
fs.writeFileSync ('package.json', JSON.stringify (pkg, null, 2));
child_process.execSync ('yarn lint', { stdio: 'inherit' });
child_process.execSync ('yarn test', { stdio: 'inherit' });
child_process.execSync ('yarn compile', { stdio: 'inherit' });
child_process.exec ('git log -1 | grep \'\\[no publish\\]\'')
.addListener ('exit', (code) => {
if (code === 0) {
console.log ('build not marked for deployment');
process.exit (1);
}
else { child_process.execSync ('yarn publish --access public'); }
});

23
lib/.eslintrc.js Normal file
View File

@ -0,0 +1,23 @@
/*
* Copyright (C) Sapphirecode - All Rights Reserved
* This file is part of Auth-Server-Helper which is released under MIT.
* See file 'LICENSE' for full license details.
* Created by Timo Hocker <timo@scode.ovh>, December 2020
*/
/* eslint-disable */
module.exports = {
env: {
commonjs: true,
es6: true,
node: true
},
extends: [ '@sapphirecode/eslint-config-ts' ],
globals: {
Atomics: 'readonly',
SharedArrayBuffer: 'readonly'
},
parserOptions: { ecmaVersion: 2018 },
rules: { 'node/no-unpublished-import': 'off' }
};

362
lib/AuthHandler.ts Normal file
View File

@ -0,0 +1,362 @@
/*
* Copyright (C) Sapphirecode - All Rights Reserved
* This file is part of Auth-Server-Helper which is released under MIT.
* See file 'LICENSE' for full license details.
* Created by Timo Hocker <timo@scode.ovh>, January 2021
*/
import { IncomingMessage, ServerResponse } from 'http';
import { to_utf8 } from '@sapphirecode/encoding-helper';
import auth from './Authority';
import { debug } from './debug';
import { build_cookie, CookieSettings } from './cookie';
const logger = debug ('auth');
interface AccessSettings {
access_token_expires_in: number
include_refresh_token?: boolean
refresh_token_expires_in?: number
redirect_to?: string
data?: unknown,
leave_open?: boolean
permissions?: string[]
}
interface AccessResult {
access_token_id: string;
refresh_token_id?: string;
}
interface AccessResponse {
token_type: string;
access_token: string;
expires_in: number;
refresh_token?: string;
refresh_expires_in?: number;
}
type AuthHandler =
(req: IncomingMessage, res: ServerResponse) => Promise<boolean>;
class AuthRequest {
public request: IncomingMessage;
public response: ServerResponse;
public is_basic: boolean;
public user: string;
public password: string;
public is_bearer: boolean;
public token?: string;
public token_data?: unknown;
public token_id?: string;
public body: string;
private _cookie?: CookieSettings;
private _refresh_cookie?: CookieSettings;
private _is_successful: boolean;
public get is_successful (): boolean {
return this._is_successful;
}
public constructor (
req: IncomingMessage,
res: ServerResponse,
body: string,
cookie?: CookieSettings,
refresh_cookie?: CookieSettings
) {
this.request = req;
this.response = res;
this.body = body;
this.is_basic = false;
this.is_bearer = false;
this.user = '';
this.password = '';
this._cookie = cookie;
this._refresh_cookie = refresh_cookie;
this._is_successful = false;
logger.extend ('constructor') ('started processing new auth request');
}
private default_header (set_content = true) {
this.response.setHeader ('Cache-Control', 'no-store');
this.response.setHeader ('Pragma', 'no-cache');
if (set_content)
this.response.setHeader ('Content-Type', 'application/json');
}
// eslint-disable-next-line max-statements, max-lines-per-function
public async allow_access ({
access_token_expires_in,
include_refresh_token,
refresh_token_expires_in,
redirect_to,
data,
leave_open,
permissions
}: AccessSettings): Promise<AccessResult> {
const log = logger.extend ('allow_access');
log ('allowed access');
this.default_header (typeof redirect_to !== 'string' && !leave_open);
const at = await auth.sign (
'access_token',
access_token_expires_in,
{ data, permissions }
);
const result: AccessResult = { access_token_id: at.id };
const res: AccessResponse = {
token_type: 'bearer',
access_token: at.signature,
expires_in: access_token_expires_in
};
const cookies = [];
if (typeof this._cookie !== 'undefined')
cookies.push (build_cookie (this._cookie, at.signature));
if (include_refresh_token) {
log ('including refresh token');
if (typeof refresh_token_expires_in !== 'number')
throw new Error ('no expiry time defined for refresh tokens');
const rt = await auth.sign (
'refresh_token',
refresh_token_expires_in,
{ data }
);
res.refresh_token = rt.signature;
res.refresh_expires_in = refresh_token_expires_in;
result.refresh_token_id = rt.id;
if (typeof this._refresh_cookie !== 'undefined')
cookies.push (build_cookie (this._refresh_cookie, rt.signature));
}
if (cookies.length > 0) {
log ('sending %d cookies', cookies.length);
this.response.setHeader (
'Set-Cookie',
cookies
);
}
this._is_successful = true;
if (typeof redirect_to === 'string') {
log ('redirecting to %s', redirect_to);
this.response.setHeader ('Location', redirect_to);
this.response.statusCode = 302;
if (!leave_open)
this.response.end ();
return result;
}
if (!leave_open) {
log ('finishing http request');
this.response.writeHead (200);
this.response.end (JSON.stringify (res));
}
return result;
}
public async allow_part (
part_token_expires_in: number,
next_module: string,
data?: Record<string, unknown>,
leave_open = false
): Promise<string> {
const log = logger.extend ('allow_part');
log ('allowed part token');
this.default_header ();
const pt = await auth.sign (
'part_token',
part_token_expires_in,
{ next_module, data }
);
const res = {
token_type: 'bearer',
part_token: pt.signature,
expires_in: part_token_expires_in
};
if (!leave_open) {
log ('finishing http request');
this.response.writeHead (200);
this.response.end (JSON.stringify (res));
}
this._is_successful = true;
return pt.id;
}
public invalid (error_description?: string, leave_open = false): void {
const log = logger.extend ('invalid');
log ('rejecting invalid request');
this.default_header ();
this.response.statusCode = 400;
if (!leave_open) {
log ('finishing http request');
this.response.end (JSON.stringify ({
error: 'invalid_request',
error_description
}));
}
}
public deny (leave_open = false): void {
const log = logger.extend ('deny');
log ('denied access');
this.default_header ();
this.response.statusCode = 401;
if (!leave_open) {
log ('finishing http request');
this.response.end (JSON.stringify ({ error: 'invalid_client' }));
}
}
}
type AuthRequestHandler = (req: AuthRequest) => Promise<void> | void;
interface CreateHandlerOptions {
refresh?: AccessSettings;
modules?: Record<string, AuthRequestHandler>;
cookie?: CookieSettings;
refresh_cookie?: CookieSettings;
parse_body?: boolean;
}
type ProcessRequestOptions = Omit<CreateHandlerOptions, 'parse_body'>
// eslint-disable-next-line max-lines-per-function, max-statements
async function process_request (
request: AuthRequest,
token: RegExpExecArray | null,
default_handler: AuthRequestHandler,
options?: ProcessRequestOptions
): Promise<void> {
const log = logger.extend ('process_request');
if (token === null)
return default_handler (request);
if ((/Basic/ui).test (token?.groups?.type as string)) {
log ('found basic login data');
request.is_basic = true;
let login = token?.groups?.token as string;
if (!login.includes (':'))
login = to_utf8 (login, 'base64');
const login_data = login.split (':');
request.user = login_data[0];
request.password = login_data[1];
return default_handler (request);
}
if ((/Bearer/ui).test (token?.groups?.type as string)) {
log ('found bearer login data');
request.is_bearer = true;
request.token = token?.groups?.token;
const token_data = await auth.verify (request.token as string);
if (!token_data.valid)
return default_handler (request);
log ('bearer token is valid');
request.token_data = token_data.data;
request.token_id = token_data.id;
if (
typeof options !== 'undefined'
&& typeof options.refresh !== 'undefined'
&& token_data.type === 'refresh_token'
) {
log ('found refresh token, emitting new access token');
request.allow_access (options.refresh);
return Promise.resolve ();
}
if (
typeof options !== 'undefined'
&& typeof options.modules !== 'undefined'
&& token_data.type === 'part_token'
&& typeof token_data.next_module !== 'undefined'
&& Object.keys (options.modules)
.includes (token_data.next_module)
) {
log ('processing module %s', token_data.next_module);
return options.modules[token_data.next_module] (request);
}
request.invalid ('invalid bearer type');
return Promise.resolve ();
}
log ('no matching login method, triggering default handler');
return default_handler (request);
}
// eslint-disable-next-line max-lines-per-function
export default function create_auth_handler (
default_handler: AuthRequestHandler,
options?: CreateHandlerOptions
): AuthHandler {
logger.extend ('create_auth_handler') ('creating new auth handler');
if (
typeof options?.cookie !== 'undefined'
&& typeof options?.refresh_cookie !== 'undefined'
&& options.cookie.name === options.refresh_cookie.name
)
throw new Error ('access and refresh cookies cannot have the same name');
return async (
req: IncomingMessage,
res: ServerResponse
): Promise<boolean> => {
const body: string = options?.parse_body
? await new Promise ((resolve) => {
let data = '';
req.on ('data', (c) => {
data += c;
});
req.on ('end', () => {
resolve (data);
});
})
: '';
const request = new AuthRequest (
req,
res,
body,
options?.cookie,
options?.refresh_cookie
);
const token = (/(?<type>\S+) (?<token>.+)/ui)
.exec (req.headers.authorization as string);
await process_request (request, token, default_handler, options);
return request.is_successful;
};
}
export {
AccessSettings,
AccessResult,
AccessResponse,
AuthRequest,
AuthRequestHandler,
CreateHandlerOptions,
AuthHandler
};

138
lib/Authority.ts Normal file
View File

@ -0,0 +1,138 @@
/*
* Copyright (C) Sapphirecode - All Rights Reserved
* This file is part of Auth-Server-Helper which is released under MIT.
* See file 'LICENSE' for full license details.
* Created by Timo Hocker <timo@scode.ovh>, December 2020
*/
import {
sign_object,
verify_signature_get_info
} from '@sapphirecode/crypto-helper';
import keystore from './KeyStore';
import blacklist from './Blacklist';
import { debug } from './debug';
import { generate_token_id } from './token_id';
const logger = debug ('authority');
// eslint-disable-next-line no-shadow
type TokenType = 'access_token' | 'none' | 'part_token' | 'refresh_token'
interface VerificationResult {
authorized: boolean;
valid: boolean;
type: TokenType;
id: string;
next_module?: string;
permissions?: string[];
data?: unknown;
error?: string;
}
interface SignatureResult {
signature: string;
id: string;
}
interface SignatureOptions
{
data?: unknown
next_module?: string,
permissions?: string[]
}
class Authority {
public async verify (key: string): Promise<VerificationResult> {
const log = logger.extend ('verify');
log ('verifying token');
const result: VerificationResult = {
authorized: false,
valid: false,
type: 'none',
permissions: [],
id: ''
};
const data = await verify_signature_get_info (
key,
async (info) => {
try {
return await keystore.get_key (info.iat / 1000, info.iss);
}
catch {
return '';
}
},
(info) => info.valid_for * 1000
);
if (data === null) {
log ('token invalid');
result.error = 'invalid signature';
return result;
}
result.id = data.id;
result.type = data.type;
log ('parsing token %s %s', result.type, result.id);
if (!(await blacklist.is_valid (data.id))) {
log ('token is blacklisted');
result.error = 'blacklisted';
return result;
}
result.valid = true;
result.authorized = result.type === 'access_token';
result.next_module = data.next_module;
result.permissions = data.permissions;
result.data = data.obj;
log (
'valid %s; targeting module %s',
result.type,
result.next_module
);
log ('permissions %o', result.permissions);
return result;
}
public async sign (
type: TokenType,
valid_for: number,
options?: SignatureOptions
): Promise<SignatureResult> {
const log = logger.extend ('sign');
log ('signing new %s', type);
const time = Date.now ();
const valid_until = time + (valid_for * 1e3);
const key = await keystore.get_sign_key (time / 1000, valid_for);
const attributes = {
id: generate_token_id (new Date (valid_until)),
iat: time,
iss: keystore.instance_id,
type,
valid_for,
valid_until,
next_module: options?.next_module,
permissions: options?.permissions
};
const signature = sign_object (options?.data, key, attributes);
log ('created token %s', attributes.id);
return { id: attributes.id, signature };
}
}
const auth = (new Authority);
export {
TokenType,
VerificationResult,
SignatureResult,
SignatureOptions,
Authority
};
export default auth;

150
lib/Blacklist.ts Normal file
View File

@ -0,0 +1,150 @@
/*
* Copyright (C) Sapphirecode - All Rights Reserved
* This file is part of Auth-Server-Helper which is released under MIT.
* See file 'LICENSE' for full license details.
* Created by Timo Hocker <timo@scode.ovh>, December 2020
*/
import { debug } from './debug';
import { redis_blacklist_store } from './RedisData/RedisBlacklistStore';
import { parse_token_id } from './token_id';
const logger = debug ('blacklist');
interface Signature {
token_id: string;
iat: number;
valid_until: Date;
}
interface ExportedSignature {
token_id: string;
iat: number;
}
class Blacklist {
private _signatures: Signature[];
private _interval: NodeJS.Timeout;
public constructor () {
this._signatures = [];
this._interval = setInterval (
this.garbage_collect.bind (this),
3600000
);
}
public async clear (
before: number = Number.POSITIVE_INFINITY
): Promise<void> {
logger.extend ('clear') ('clearing blacklist');
for (let i = this._signatures.length - 1; i >= 0; i--) {
if (this._signatures[i].iat < before) {
// eslint-disable-next-line no-await-in-loop
await this.remove_signature (i);
}
}
}
public async add_signature (token_id: string): Promise<void> {
logger.extend ('add_signature') ('blacklisting signature %s', token_id);
const parsed = parse_token_id (token_id);
this._signatures.push ({
iat: Date.now (),
token_id,
valid_until: parsed.valid_until
});
await redis_blacklist_store.add (token_id, parsed.valid_until);
}
public async remove_signature (signature: number | string): Promise<void> {
const log = logger.extend ('remove_signature');
log ('removing signature from blacklist %s', signature);
let key = '';
if (typeof signature === 'string') {
log ('received string, searching through signatures');
key = signature;
for (let i = this._signatures.length - 1; i >= 0; i--) {
if (this._signatures[i].token_id === signature) {
log ('removing sigature %s at %d', signature, i);
this._signatures.splice (i, 1);
}
}
}
else {
log (
'received index, removing signature %s at index %s',
this._signatures[signature].token_id,
signature
);
key = this._signatures[signature].token_id;
this._signatures.splice (signature, 1);
}
await redis_blacklist_store.remove (key);
}
public async is_valid (hash: string): Promise<boolean> {
const log = logger.extend ('is_valid');
log ('checking signature for blacklist entry %s', hash);
for (const sig of this._signatures) {
if (sig.token_id === hash) {
log ('found matching blacklist entry');
return false;
}
}
log ('signature is not blacklisted locally, checking redis');
if (await redis_blacklist_store.get (hash)) {
log ('signature is blacklisted in redis');
return false;
}
log ('signature is not blacklisted');
return true;
}
public export_blacklist (): ExportedSignature[] {
logger.extend ('export_blacklist') ('exporting blacklist');
return this._signatures.map ((v) => ({
iat: v.iat,
token_id: v.token_id
}));
}
public import_blacklist (data: ExportedSignature[]): void {
logger.extend ('import_blacklist') (
'importing %d blacklist entries',
data.length
);
for (const token of data) {
const parsed = parse_token_id (token.token_id);
this._signatures.push ({
token_id: token.token_id,
iat: token.iat,
valid_until: parsed.valid_until
});
}
}
public sync_redis (url: string): void {
redis_blacklist_store.connect (url);
}
private async garbage_collect (): Promise<void> {
const log = logger.extend ('garbage_collect');
const time = new Date;
log ('removing signatures expired before', time);
for (let i = this._signatures.length - 1; i >= 0; i--) {
if (this._signatures[i].valid_until < time) {
log ('signature %s expired', this._signatures[i].token_id);
await this.remove_signature (i);
}
}
}
}
const bl = (new Blacklist);
export { Blacklist };
export default bl;

270
lib/Gateway.ts Normal file
View File

@ -0,0 +1,270 @@
/*
* Copyright (C) Sapphirecode - All Rights Reserved
* This file is part of Auth-Server-Helper which is released under MIT.
* See file 'LICENSE' for full license details.
* Created by Timo Hocker <timo@scode.ovh>, December 2020
*/
import { IncomingMessage, ServerResponse } from 'http';
import authority from './Authority';
import { AuthRequest, AccessSettings } from './AuthHandler';
import { debug } from './debug';
import { extract_cookie, CookieSettings } from './cookie';
import blacklist from './Blacklist';
const logger = debug ('gateway');
type AnyFunc = (...args: unknown[]) => unknown;
type Gateway = (
req: IncomingMessage,
res: ServerResponse,
next: AnyFunc
) => unknown;
interface RefreshSettings extends AccessSettings {
leave_open?: never;
redirect_to?: never;
data?: never;
}
interface GatewayOptions {
redirect_url?: string;
cookie?: CookieSettings;
refresh_cookie?: CookieSettings;
refresh_settings?: RefreshSettings;
require_permissions?: string[];
}
interface ConnectionInfo {
token_id: string
token_data: unknown
permissions: string[]
}
class GatewayClass {
private _options: GatewayOptions;
public constructor (options: GatewayOptions = {}) {
const log = logger.extend ('constructor');
log ('creating new gateway');
if (
typeof options?.cookie !== 'undefined'
&& typeof options?.refresh_cookie !== 'undefined'
&& options.cookie.name === options.refresh_cookie.name
)
throw new Error ('access and refresh cookies cannot have the same name');
this._options = options;
}
public deny (res: ServerResponse): void {
logger.extend ('deny') ('denied http request');
res.statusCode = 403;
res.end ();
}
public redirect (res: ServerResponse): void {
const log = logger.extend ('redirect');
log ('redirecting http request to %s', this._options.redirect_url);
if (typeof this._options.redirect_url !== 'string') {
log ('no redirect url defined');
this.deny (res);
return;
}
res.statusCode = 302;
res.setHeader ('Location', this._options.redirect_url);
res.end ();
}
public get_header_auth (req: IncomingMessage): string | null {
const log = logger.extend ('get_header_auth');
log ('extracting authorization header');
const auth_header = req.headers.authorization;
const auth = (/(?<type>\w+) (?<data>.*)/u).exec (auth_header || '');
if (auth === null)
return null;
if (auth.groups?.type !== 'Bearer')
return null;
log ('found bearer token');
return auth.groups?.data;
}
public async try_access (req: IncomingMessage): Promise<boolean> {
const log = logger.extend ('try_access');
log ('authenticating incoming request');
let auth = this.get_header_auth (req);
if (auth === null)
auth = extract_cookie (this._options.cookie?.name, req.headers.cookie);
if (auth === null) {
log ('found no auth token');
return false;
}
const ver = await authority.verify (auth);
log ('setting connection info');
const con = req.connection as unknown as Record<string, unknown>;
con.auth = {
token_id: ver.id,
token_data: ver.data,
permissions: ver.permissions
};
log ('token valid: %s', ver.authorized);
return ver.authorized;
}
public async try_refresh (
req: IncomingMessage,
res: ServerResponse
): Promise<boolean> {
const log = logger.extend ('try_refresh');
if (
typeof this._options.refresh_cookie === 'undefined'
|| typeof this._options.refresh_settings === 'undefined'
)
return false;
log ('trying to apply refresh token');
const refresh = extract_cookie (
this._options.refresh_cookie.name,
req.headers.cookie
);
if (refresh === null) {
log ('could not find refresh token');
return false;
}
const ver = await authority.verify (refresh);
if (ver.type === 'refresh_token' && ver.valid) {
log ('refresh token valid, generating new tokens');
const auth_request = new AuthRequest (
req,
res,
'',
this._options.cookie,
this._options.refresh_cookie
);
const refresh_result = await auth_request.allow_access ({
...this._options.refresh_settings,
data: ver.data,
leave_open: true
});
log ('setting connection info');
const con = req.connection as unknown as Record<string, unknown>;
con.auth = {
token_id: refresh_result.access_token_id,
token_data: ver.data,
permissions: ver.permissions
};
log ('tokens refreshed');
return true;
}
log ('refresh token invalid');
return false;
}
public async authenticate (
req: IncomingMessage,
res: ServerResponse
): Promise<boolean> {
const log = logger.extend ('authenticate');
log ('trying to authenticate http request');
if (await this.try_access (req)) {
log ('authenticated via access_token');
return true;
}
if (await this.try_refresh (req, res)) {
log ('authenticated via refresh_token');
return true;
}
log ('could not verify session');
return false;
}
public check_permissions (
req: IncomingMessage,
permissions = this._options.require_permissions || []
): boolean {
for (const perm of permissions) {
if (!this.has_permission (req, perm))
return false;
}
return true;
}
public has_permission (req: IncomingMessage, permission: string) {
const info = this.get_info (req);
return info.permissions.includes (permission);
}
public async process_request (
req: IncomingMessage,
res: ServerResponse,
next: AnyFunc
): Promise<unknown> {
const log = logger.extend ('process_request');
log ('processing incoming http request');
if (await this.authenticate (req, res)) {
log ('authentification successful');
log ('checking permissions');
if (!this.check_permissions (req))
return this.redirect (res);
log ('authorization successful. calling next handler');
return next ();
}
log ('failed to authenticate, redirecting client');
return this.redirect (res);
}
public async logout (req: IncomingMessage): Promise<void> {
const log = logger.extend ('logout');
log ('invalidating all submitted tokens');
const auth_strings = [
this.get_header_auth (req),
extract_cookie (this._options.cookie?.name, req.headers.cookie),
extract_cookie (this._options.refresh_cookie?.name, req.headers.cookie)
];
const tokens = (
await Promise.all (
auth_strings
.filter ((v) => v !== null)
.map ((v) => authority.verify (v as string))
)
).filter ((v) => v.valid);
log ('found %d tokens: %O', tokens.length, tokens);
for (const token of tokens) {
// eslint-disable-next-line no-await-in-loop
await blacklist.add_signature (token.id);
}
log ('complete');
}
public get_info (req: IncomingMessage): ConnectionInfo {
const conn = req.connection as unknown as Record<string, unknown>;
const auth = conn.auth as Record<string, unknown>;
return {
token_id: auth.token_id as string,
token_data: auth.token_data,
permissions: (auth.permissions as string[]) || []
};
}
}
export default function create_gateway (options: GatewayOptions): Gateway {
const g = new GatewayClass (options);
return g.process_request.bind (g);
}
export { AnyFunc, Gateway, GatewayOptions, GatewayClass, RefreshSettings };

23
lib/Key.ts Normal file
View File

@ -0,0 +1,23 @@
/*
* Copyright (C) Sapphirecode - All Rights Reserved
* This file is part of Auth-Server-Helper which is released under MIT.
* See file 'LICENSE' for full license details.
* Created by Timo Hocker <timo@scode.ovh>, August 2022
*/
export interface Key {
key: string;
valid_until: number;
}
export interface LabelledKey extends Key {
index: string;
}
export interface KeyPair {
private_key?: Key;
public_key: Key;
}
export type KeyStoreData = Record<string, KeyPair>;
export type KeyStoreExport = LabelledKey[];

192
lib/KeyStore.ts Normal file
View File

@ -0,0 +1,192 @@
/*
* Copyright (C) Sapphirecode - All Rights Reserved
* This file is part of Auth-Server-Helper which is released under MIT.
* See file 'LICENSE' for full license details.
* Created by Timo Hocker <timo@scode.ovh>, December 2020
*/
import { generate_keypair, random_hex } from '@sapphirecode/crypto-helper';
import { to_b58 } from '@sapphirecode/encoding-helper';
import { debug } from './debug';
import { KeyStoreData, KeyStoreExport } from './Key';
import { redis_key_store } from './RedisData/RedisKeyStore';
const logger = debug ('keystore');
const renew_interval = 3600;
class KeyStore {
private _keys: KeyStoreData = {};
private _interval: NodeJS.Timeout;
private _instance: string;
public get instance_id (): string {
return this._instance;
}
public constructor () {
this._interval = setInterval (() => {
this.garbage_collect ();
}, renew_interval * 1000);
this._instance = to_b58 (random_hex (16), 'hex');
logger.extend ('constructor') (
'created keystore instance %s',
this._instance
);
}
private get_index (iat: number, instance = this._instance): string {
return instance + Math.floor (iat / renew_interval)
.toFixed (0);
}
private async create_key (index: string, valid_for: number): Promise<void> {
const log = logger.extend ('create_key');
log ('generating new key');
const time = (new Date)
.getTime ();
const pair = await generate_keypair ();
const result = {
private_key: {
key: pair.private_key,
valid_until: time + (renew_interval * 1000)
},
public_key: {
key: pair.public_key,
valid_until: time + (valid_for * 1000)
}
};
await redis_key_store.set ({ ...result.public_key, index });
this._keys[index] = result;
}
private garbage_collect (): void {
const log = logger.extend ('garbage_collect');
const time = (new Date)
.getTime ();
const keys = Object.keys (this._keys);
for (const index of keys) {
const entry = this._keys[index];
if (typeof entry.private_key !== 'undefined'
&& entry.private_key.valid_until < time
) {
log ('deleting expired private key');
delete entry.private_key;
}
if (entry.public_key.valid_until < time) {
log ('deleting expired key pair');
delete this._keys[index];
}
}
}
public async get_sign_key (
iat: number,
valid_for: number,
instance?: string
): Promise<string> {
const log = logger.extend ('get_sign_key');
log (
'querying key from %s for timestamp %d, valid for %d',
instance,
iat,
valid_for
);
if (valid_for <= 0)
throw new Error ('cannot create infinitely valid key');
if ((iat + 1) * 1000 < (new Date)
.getTime ())
throw new Error ('cannot access already expired keys');
const index = this.get_index (iat, instance);
const valid_until = (new Date)
.getTime () + (valid_for * 1000);
if (typeof this._keys[index] !== 'undefined') {
log ('loading existing key');
const key = this._keys[index];
if (typeof key.private_key === 'undefined')
throw new Error ('cannot access already expired keys');
if (key.public_key.valid_until < valid_until) {
log ('updating key valid timespan to match new value');
key.public_key.valid_until = valid_until;
}
return key.private_key?.key as string;
}
log ('key does not exist, creating a new one');
await this.create_key (index, valid_for);
return this._keys[index].private_key?.key as string;
}
public async get_key (iat: number, instance?: string): Promise<string> {
const log = logger.extend ('get_key');
log ('querying public key from %s for timestamp %d', instance, iat);
const index = this.get_index (iat, instance);
let key = null;
if (typeof this._keys[index] === 'undefined')
key = await redis_key_store.get (index);
else
key = this._keys[index].public_key;
if (key === null)
throw new Error ('key could not be found');
return key.key;
}
public export_verification_data (): KeyStoreExport {
const log = logger.extend ('export_verification_data');
log ('exporting public keys');
log ('cleaning up before export');
this.garbage_collect ();
const out: KeyStoreExport = [];
for (const index of Object.keys (this._keys)) {
log ('exporting key %s', index);
out.push ({ ...this._keys[index].public_key, index });
}
return out;
}
public import_verification_data (data: KeyStoreExport): void {
const log = logger.extend ('import_verification_data');
log ('importing %d public keys', data.length);
for (const key of data) {
log ('importing key %s', key.index);
if (typeof this._keys[key.index] !== 'undefined')
throw new Error ('cannot import to the same instance');
this._keys[key.index] = {
public_key: {
key: key.key,
valid_until: key.valid_until
}
};
}
log ('running garbage collector');
this.garbage_collect ();
}
public reset_instance (): void {
logger.extend ('reset_instance') ('resetting keystore');
this._instance = to_b58 (random_hex (16), 'hex');
this._keys = {};
redis_key_store.disconnect ();
}
public sync_redis (url: string): void {
redis_key_store.connect (url);
}
}
const ks: KeyStore = (new KeyStore);
export default ks;
export { KeyStore };

64
lib/Redis.ts Normal file
View File

@ -0,0 +1,64 @@
/*
* Copyright (C) Sapphirecode - All Rights Reserved
* This file is part of Auth-Server-Helper which is released under MIT.
* See file 'LICENSE' for full license details.
* Created by Timo Hocker <timo@scode.ovh>, August 2022
*/
import IORedis from 'ioredis';
import { debug } from './debug';
const logger = debug ('redis');
export class Redis {
private _redis: IORedis | null = null;
public connect (url: string): void {
const log = logger.extend ('connect');
log ('connecting to redis instance %s', url);
if (this._redis !== null) {
log ('disconnecting existing redis client');
this.disconnect ();
}
this._redis = new IORedis (url);
this._redis.on ('connect', () => {
log ('connected');
});
this._redis.on ('ready', () => {
log ('ready');
});
this._redis.on ('error', (err) => {
log ('error %o', err);
});
this._redis.on ('reconnecting', () => {
log ('reconnecting');
});
this._redis.on ('end', () => {
log ('connection ended');
});
}
public disconnect (): void {
const log = logger.extend ('disconnect');
log ('disconnecting redis client');
if (this._redis === null) {
log ('redis is inactive, skipping');
return;
}
this._redis.quit ();
this._redis = null;
log ('done');
}
protected get redis (): IORedis {
if (this._redis === null)
throw new Error ('redis is not connected');
return this._redis;
}
protected get is_active (): boolean {
return this._redis !== null;
}
}

View File

@ -0,0 +1,53 @@
/*
* Copyright (C) Sapphirecode - All Rights Reserved
* This file is part of Auth-Server-Helper which is released under MIT.
* See file 'LICENSE' for full license details.
* Created by Timo Hocker <timo@scode.ovh>, August 2022
*/
import { debug } from '../debug';
import { Redis } from '../Redis';
const logger = debug ('RedisBlacklistStore');
export class RedisBlacklistStore extends Redis {
public async add (key: string, valid_until: Date): Promise<void> {
const log = logger.extend ('set');
log ('trying to add key %s to redis blacklist', key);
if (!this.is_active) {
log ('redis is inactive, skipping');
return;
}
await this.redis.setex (
`blacklist_${key}`,
Math.floor ((valid_until.getTime () - Date.now ()) / 1000),
1
);
log ('saved key');
}
public async remove (key: string): Promise<void> {
const log = logger.extend ('remove');
log ('removing key %s from redis', key);
if (!this.is_active) {
log ('redis is inactive, skipping');
return;
}
await this.redis.del (`blacklist_${key}`);
log ('removed key');
}
public async get (key: string): Promise<boolean> {
const log = logger.extend ('get');
log ('trying to find key %s in redis blacklist', key);
if (!this.is_active) {
log ('redis is inactive, skipping');
return false;
}
const res = await this.redis.exists (`blacklist_${key}`) === 1;
log ('found key %s', res);
return res;
}
}
export const redis_blacklist_store = new RedisBlacklistStore;

View File

@ -0,0 +1,52 @@
/*
* Copyright (C) Sapphirecode - All Rights Reserved
* This file is part of Auth-Server-Helper which is released under MIT.
* See file 'LICENSE' for full license details.
* Created by Timo Hocker <timo@scode.ovh>, August 2022
*/
import { debug } from '../debug';
import { LabelledKey } from '../Key';
import { Redis } from '../Redis';
const logger = debug ('RedisKeyStore');
export class RedisKeyStore extends Redis {
public async set (value: LabelledKey): Promise<void> {
const log = logger.extend ('set');
log ('trying to set key %s to redis', value.index);
if (!this.is_active) {
log ('redis is inactive, skipping');
return;
}
const valid_for = Math.floor (
(value.valid_until - (new Date)
.getTime ()) / 1000
);
log ('key is valid for %d seconds', valid_for);
await this.redis.setex (
`keystore_${value.index}`,
valid_for,
JSON.stringify (value)
);
log ('saved key');
}
public async get (index: string): Promise<LabelledKey | null> {
const log = logger.extend ('get');
log ('trying to get key %s from redis', index);
if (!this.is_active) {
log ('redis is inactive, skipping');
return null;
}
const res = await this.redis.get (`keystore_${index}`);
if (res === null) {
log ('key not found in redis');
return null;
}
log ('key found');
return JSON.parse (res);
}
}
export const redis_key_store = new RedisKeyStore;

86
lib/cookie.ts Normal file
View File

@ -0,0 +1,86 @@
/*
* Copyright (C) Sapphirecode - All Rights Reserved
* This file is part of Auth-Server-Helper which is released under MIT.
* See file 'LICENSE' for full license details.
* Created by Timo Hocker <timo@scode.ovh>, August 2022
*/
import { run_regex } from '@sapphirecode/utilities';
import { debug } from './debug';
const logger = debug ('cookies');
type SameSiteValue = 'Lax' | 'None' | 'Strict';
interface CookieSettings {
name: string;
secure?: boolean;
http_only?: boolean;
same_site?: SameSiteValue|null;
expires?: string;
max_age?: number;
domain?: string;
path?: string;
}
const default_settings: Omit<CookieSettings, 'name'> = {
secure: true,
http_only: true,
same_site: 'Strict'
};
function build_cookie (
settings: CookieSettings,
value: string
): string {
const local_settings = { ...default_settings, ...settings };
const sections = [ `${local_settings.name}=${value}` ];
if (local_settings.secure)
sections.push ('Secure');
if (local_settings.http_only)
sections.push ('HttpOnly');
if (
typeof local_settings.same_site !== 'undefined'
&& local_settings.same_site !== null
)
sections.push (`SameSite=${local_settings.same_site}`);
if (typeof local_settings.expires !== 'undefined')
sections.push (`Expires=${local_settings.expires}`);
if (typeof local_settings.max_age !== 'undefined')
sections.push (`Max-Age=${local_settings.max_age}`);
if (typeof local_settings.domain !== 'undefined')
sections.push (`Domain=${local_settings.domain}`);
if (typeof local_settings.path !== 'undefined')
sections.push (`Path=${local_settings.path}`);
return sections.join ('; ');
}
function extract_cookie (
name: string|undefined,
header: string|undefined
): string| null {
const log = logger.extend ('extract_cookie');
log (`extracting cookie ${name}`);
const cookie_regex = /(?:^|;)\s*(?<name>[^;=]+)=(?<value>[^;]+)/gu;
let result = null;
run_regex (
cookie_regex,
header,
(res: RegExpMatchArray) => {
log ('parsing cookie %s', res.groups?.name);
if (res.groups?.name === name) {
log ('found cookie');
result = res.groups?.value as string;
}
}
);
return result;
}
export { build_cookie, extract_cookie, SameSiteValue, CookieSettings };

15
lib/debug.ts Normal file
View File

@ -0,0 +1,15 @@
/*
* Copyright (C) Sapphirecode - All Rights Reserved
* This file is part of Auth-Server-Helper which is released under MIT.
* See file 'LICENSE' for full license details.
* Created by Timo Hocker <timo@scode.ovh>, August 2022
*/
import build_debug from 'debug';
function debug (scope: string): build_debug.Debugger {
const namespace = `sapphirecode:auth-server-helper:${scope}`;
return build_debug (namespace);
}
export { debug };

74
lib/index.ts Normal file
View File

@ -0,0 +1,74 @@
/*
* Copyright (C) Sapphirecode - All Rights Reserved
* This file is part of Auth-Server-Helper which is released under MIT.
* See file 'LICENSE' for full license details.
* Created by Timo Hocker <timo@scode.ovh>, January 2021
*/
/* eslint-disable import/no-namespace */
import create_auth_handler, {
AccessResponse,
CreateHandlerOptions,
AuthRequestHandler,
AuthRequest,
AccessSettings,
AccessResult,
AuthHandler
} from './AuthHandler';
import authority, {
VerificationResult,
TokenType,
SignatureResult,
SignatureOptions,
Authority
} from './Authority';
import blacklist, { Blacklist } from './Blacklist';
import create_gateway, {
GatewayOptions,
GatewayClass,
Gateway,
AnyFunc,
RefreshSettings
} from './Gateway';
import keystore, { KeyStore } from './KeyStore';
import {
KeyStoreExport,
LabelledKey, Key
} from './Key';
import {
CookieSettings,
SameSiteValue
} from './cookie';
export {
create_gateway,
create_auth_handler,
blacklist,
authority,
keystore,
AccessResponse,
CreateHandlerOptions,
AuthRequestHandler,
AuthRequest,
AuthHandler,
AccessSettings,
AccessResult,
VerificationResult,
TokenType,
SignatureResult,
SignatureOptions,
Authority,
Blacklist,
GatewayOptions,
GatewayClass,
Gateway,
RefreshSettings,
AnyFunc,
KeyStore,
KeyStoreExport,
LabelledKey,
Key,
CookieSettings,
SameSiteValue
};

21
lib/token_id.ts Normal file
View File

@ -0,0 +1,21 @@
import { create_salt } from '@sapphirecode/crypto-helper';
import { to_b58 } from '@sapphirecode/encoding-helper';
export function generate_token_id (valid_until: Date) {
const salt = create_salt ();
return `${to_b58 (salt, 'hex')};${valid_until.toISOString ()}`;
}
export function parse_token_id (id: string) {
// eslint-disable-next-line max-len
const regex = /^(?<hash>[A-HJ-NP-Za-km-z1-9]+);(?<date>\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d{3}Z)$/u;
const result = regex.exec (id);
if (result === null)
throw new Error (`invalid token id ${id}`);
if (typeof result.groups === 'undefined')
throw new Error ('invalid state');
return {
hash: result.groups.hash as string,
valid_until: new Date (result.groups.date as string)
};
}

View File

@ -1,52 +0,0 @@
/*
* Copyright (C) Sapphirecode - All Rights Reserved
* This file is part of Auth-Server-Helper which is released under MIT.
* See file 'LICENSE' for full license details.
* Created by Timo Hocker <timo@scode.ovh>, March 2020
*/
/* eslint-disable no-magic-numbers */
// @ts-nocheck
'use strict';
const express = require ('express');
const auth = require ('./index');
const consts = require ('@scode/consts');
const crypto = require ('@scode/crypto-helper');
const password_helper = require ('@scode/password-helper');
/**
* start the server
*/
async function start_server () {
const app = express ();
const name = 'testuser';
const salt = crypto.create_salt ();
const password = await password_helper.hash (
crypto.hash_sha512 ('foo', salt)
);
const user = { name, salt, password };
app.use (auth ((user_name) => {
if (user.name === user_name)
return user;
return null;
}, [
/noauthreg/u,
{ method: 'POST', regex: /noauthobj/u }
]));
app.use ((req, res) => {
res.status (consts.http.status_ok)
.end ('foo');
});
app.listen (3000);
return app;
}
module.exports = { start_server };

View File

@ -1,34 +1,58 @@
{
"name": "@sapphirecode/auth-server-helper",
"version": "1.0.0",
"main": "index.js",
"author": "Timo Hocker <t-hocker@web.de>",
"version": "4.1.1",
"main": "dist/lib/index.js",
"author": {
"name": "Timo Hocker",
"email": "timo@scode.ovh"
},
"repository": {
"type": "git",
"url": "https://git.scode.ovh/timo/auth-server-helper.git"
},
"bugs": "https://git.scode.ovh/timo/auth-server-helper",
"description": "authentication middleware for node http and express",
"license": "MIT",
"devDependencies": {
"@sapphirecode/auth-client-helper": "^1.0.6",
"@sapphirecode/crypto-helper": "^1.1.9",
"@sapphirecode/eslint-config": "^2.0.2",
"@sapphirecode/password-helper": "^1.0.3",
"@stryker-mutator/core": "^3.0.2",
"@stryker-mutator/javascript-mutator": "^3.0.2",
"ava": "^3.5.0",
"eslint": "^6.8.0",
"express": "^4.17.1",
"node-fetch": "^2.6.0",
"nyc": "^15.0.0"
"@sapphirecode/eslint-config-ts": "^1.1.27",
"@stryker-mutator/core": "^6.1.2",
"@stryker-mutator/jasmine-runner": "^6.1.2",
"@types/debug": "^4.1.7",
"@types/jasmine": "^4.0.3",
"@types/node": "^18.6.4",
"eslint": "^8.21.0",
"jasmine": "^4.3.0",
"nyc": "^15.1.0",
"ts-node": "^10.9.1",
"typescript": "^4.1.2"
},
"scripts": {
"lint": "eslint . --ext .js,.jsx,.ts,.tsx,.vue,.mjs",
"test": "nyc ava",
"ci": "yarn && node jenkins.js",
"pretest": "yarn compile",
"test": "nyc jasmine --config=\"jasmine.json\"",
"mutate": "stryker run",
"compile": "tsc --allowJs --declaration --emitDeclarationOnly index.js"
},
"dependencies": {
"@sapphirecode/consts": "^1.0.3"
"precompile": "rm -rf dist",
"compile": "tsc"
},
"files": [
"LICENSE",
"index.js"
]
"lib/**/*.ts",
"dist/**/*.js",
"dist/**/*.d.ts",
"dist/**/*.map"
],
"keywords": [
"authentication",
"express",
"middleware"
],
"dependencies": {
"@sapphirecode/crypto-helper": "^2.0.0",
"@sapphirecode/encoding-helper": "^1.1.0",
"@sapphirecode/utilities": "^1.8.8",
"debug": "^4.3.3",
"ioredis": "^5.2.2"
},
"engines": {
"node": ">=10.0.0"
}
}

View File

@ -2,7 +2,7 @@
* Copyright (C) Sapphirecode - All Rights Reserved
* This file is part of Auth-Server-Helper which is released under MIT.
* See file 'LICENSE' for full license details.
* Created by Timo Hocker <timo@scode.ovh>, March 2020
* Created by Timo Hocker <timo@scode.ovh>, May 2020
*/
'use strict';
@ -11,14 +11,14 @@
* @type {import('@stryker-mutator/api/core').StrykerOptions}
*/
module.exports = {
mutator: 'javascript',
packageManager: 'yarn',
reporters: [
'clear-text',
'progress'
],
testRunner: 'command',
transpilers: [],
coverageAnalysis: 'all',
mutate: [ 'index.js' ]
testRunner: 'jasmine',
jasmineConfigFile: 'jasmine.json',
coverageAnalysis: 'perTest',
mutate: [ 'lib/*.ts' ],
tsconfigFile: 'tsconfig.json'
};

22
test/.eslintrc.js Normal file
View File

@ -0,0 +1,22 @@
/*
* Copyright (C) Sapphirecode - All Rights Reserved
* This file is part of Auth-Server-Helper which is released under MIT.
* See file 'LICENSE' for full license details.
* Created by Timo Hocker <timo@scode.ovh>, December 2020
*/
/* eslint-disable */
module.exports = {
env: {
commonjs: true,
es6: true,
node: true
},
extends: [ '@sapphirecode/eslint-config-ts' ],
globals: {
Atomics: 'readonly',
SharedArrayBuffer: 'readonly'
},
parserOptions: { ecmaVersion: 2018 }
};

81
test/Helper.ts Normal file
View File

@ -0,0 +1,81 @@
/*
* Copyright (C) Sapphirecode - All Rights Reserved
* This file is part of Auth-Server-Helper which is released under MIT.
* See file 'LICENSE' for full license details.
* Created by Timo Hocker <timo@scode.ovh>, January 2021
*/
/* eslint-disable no-console */
import http from 'http';
import ks from '../lib/KeyStore';
export class Response extends http.IncomingMessage {
body?: string;
}
export function get (
// eslint-disable-next-line default-param-last
headers: http.OutgoingHttpHeaders = {},
body?: string|null,
path = ''
): Promise<Response> {
return new Promise ((resolve) => {
const req = http.request (`http://localhost:3000${path}`, {
headers,
method: typeof body === 'string' ? 'POST' : 'GET'
}, (res: Response) => {
let data = '';
res.on ('data', (d) => {
data += d;
});
res.on ('end', () => {
res.body = data;
resolve (res);
});
});
if (typeof body === 'string')
req.write (body);
req.end ();
});
}
export function modify_signature (signature: string): string {
const dec = signature.split ('.');
dec[1] = '';
return dec.join ('.');
}
/* eslint-disable dot-notation */
export function assert_keystore_state (): void {
const set = ks['_keys'];
const keys = Object.keys (set);
if (keys.length !== 0) {
const has_sign = keys.filter (
(v) => typeof set[v].private_key !== 'undefined'
).length;
console.warn ('keystore gc not running!');
console.warn (`${keys.length} keys with ${has_sign} signature keys left`);
ks['_keys'] = {};
}
}
/* eslint-enable dot-notation */
export function clock_setup (): void {
assert_keystore_state ();
const date = (new Date);
date.setHours (0, 0, 2, 0);
jasmine.clock ()
.install ();
jasmine.clock ()
.mockDate (date);
}
export function clock_finalize (): void {
jasmine.clock ()
.tick (30 * 24 * 60 * 60 * 1000);
// eslint-disable-next-line dot-notation
ks['garbage_collect'] ();
jasmine.clock ()
.uninstall ();
}

View File

@ -1,75 +0,0 @@
/*
* Copyright (C) Sapphirecode - All Rights Reserved
* This file is part of Auth-Server-Helper which is released under MIT.
* See file 'LICENSE' for full license details.
* Created by Timo Hocker <timo@scode.ovh>, March 2020
*/
// @ts-nocheck
'use strict';
const test = require ('ava');
const mock_server = require ('../mock_server');
const client = require ('@scode/auth-client-helper');
const consts = require ('@scode/consts');
const fetch = require ('node-fetch');
test.before (async () => {
await mock_server.start_server ();
});
test ('login', async (t) => {
const session = await client.login (
'testuser',
'foo',
'http://localhost:3000'
);
t.is (typeof session, 'string');
const resp = await fetch ('http://localhost:3000', { headers: { session } });
t.is (resp.status, consts.http.status_ok);
t.is (await resp.text (), 'foo');
});
test ('allow access to excluded paths', async (t) => {
const resp = await fetch ('http://localhost:3000/noauthreg');
t.is (resp.status, consts.http.status_ok);
t.is (await resp.text (), 'foo');
});
test ('allow access to excluded paths with correct method', async (t) => {
const resp = await fetch (
'http://localhost:3000/noauthobj',
{ method: 'POST' }
);
t.is (resp.status, consts.http.status_ok);
t.is (await resp.text (), 'foo');
});
test ('reject access to excluded paths with wrong method', async (t) => {
const resp = await fetch (
'http://localhost:3000/noauthobj'
);
t.is (resp.status, consts.http.status_unauthorized);
});
test ('reject invalid user', async (t) => {
await t.throwsAsync (client.login (
'foo',
'foo',
'http://localhost:3000'
));
});
test ('reject invalid password', async (t) => {
await t.throwsAsync (client.login (
'testuser',
'bar',
'http://localhost:3000'
));
});

378
test/spec/AuthHandler.ts Normal file
View File

@ -0,0 +1,378 @@
/*
* Copyright (C) Sapphirecode - All Rights Reserved
* This file is part of Auth-Server-Helper which is released under MIT.
* See file 'LICENSE' for full license details.
* Created by Timo Hocker <timo@scode.ovh>, January 2021
*/
/* eslint-disable max-lines */
import http, { IncomingMessage, ServerResponse } from 'http';
import { to_b64 } from '@sapphirecode/encoding-helper';
import auth from '../../lib/Authority';
import {
clock_finalize,
clock_setup,
get, modify_signature, Response
} from '../Helper';
import { create_auth_handler } from '../../lib/index';
import { build_cookie, extract_cookie } from '../../lib/cookie';
const expires_seconds = 600;
const refresh_expires_seconds = 3600;
const part_expires_seconds = 60;
interface CheckHeaderResult {
at: string;
rt?: string;
data: Record<string, unknown>;
}
function check_headers (resp: Response): CheckHeaderResult {
const data = JSON.parse (resp.body as string);
const at = data.access_token;
const rt = data.refresh_token;
expect (resp.headers['cache-control'])
.toEqual ('no-store');
expect (resp.headers.pragma)
.toEqual ('no-cache');
return { data, at, rt };
}
async function check_token (token: string|null, type: string): Promise<void> {
const v = await auth.verify (token || '');
expect (v.valid)
.toEqual (true);
expect (v.authorized)
.toEqual (type === 'access_token');
expect (v.type)
.toEqual (type);
expect (token)
.toMatch (/^[0-9a-z-._~+/]+$/ui);
}
// eslint-disable-next-line max-lines-per-function
describe ('auth handler', () => {
let server: http.Server|null = null;
// eslint-disable-next-line max-lines-per-function
beforeAll (() => {
clock_setup ();
// eslint-disable-next-line complexity, max-lines-per-function
const ah = create_auth_handler (async (req) => {
if (!req.is_basic && !req.is_bearer) {
let body_auth = false;
try {
const data = JSON.parse (req.body);
if (data.username === 'foo' && data.password === 'bar') {
req.allow_access ({
access_token_expires_in: expires_seconds,
include_refresh_token: true,
refresh_token_expires_in: refresh_expires_seconds
});
body_auth = true;
}
}
catch {
body_auth = false;
}
if (!body_auth)
req.invalid ('unknown authorization type');
}
else if (req.is_bearer) {
req.deny ();
}
else if (req.user === 'foo' && req.password === 'bar') {
req.allow_access ({
access_token_expires_in: expires_seconds,
include_refresh_token: true,
refresh_token_expires_in: refresh_expires_seconds
});
}
else if (req.user === 'part' && req.password === 'bar') {
req.allow_part (part_expires_seconds, 'two_factor');
}
else if (req.user === 'red' && req.password === 'irect') {
req.allow_access ({
access_token_expires_in: expires_seconds,
redirect_to: '/redirected'
});
}
else if (req.user === 'leave' && req.password === 'open') {
req.response.setHeader ('Content-Type', 'text/plain');
await req.allow_access ({
access_token_expires_in: expires_seconds,
leave_open: true
});
req.response.write ('custom response, ');
(req.response.connection as unknown as Record<string, unknown>)
.append_flag = true;
}
else {
req.deny ();
}
}, {
cookie: { name: 'cookie_jar' },
refresh_cookie: { name: 'mint_cookies' },
refresh: {
access_token_expires_in: expires_seconds,
refresh_token_expires_in: refresh_expires_seconds,
include_refresh_token: true
},
modules: {
two_factor (request) {
if (request.body === 'letmein') {
request.allow_access ({
access_token_expires_in: expires_seconds,
include_refresh_token: true,
refresh_token_expires_in: refresh_expires_seconds
});
}
else { request.deny (); }
}
},
parse_body: true
});
server = http.createServer (async (
req: IncomingMessage,
res: ServerResponse
) => {
const is_successful = await ah (req, res);
if ((res.connection as unknown as Record<string, unknown>).append_flag)
res.end (String (is_successful));
});
server.listen (3000);
});
afterAll (() => {
if (server === null)
throw new Error ('server is null');
server.close ();
clock_finalize ();
});
it ('auth test sequence', async () => {
// get initial access and refresh tokens
const resp1 = await get ({ authorization: 'Basic foo:bar' });
expect (resp1.statusCode)
.toEqual (200);
const res1 = check_headers (resp1);
expect (res1.data.token_type)
.toEqual ('bearer');
expect (resp1.headers['set-cookie'])
.toContain (build_cookie ({ name: 'cookie_jar' }, res1.at as string));
expect (resp1.headers['set-cookie'])
.toContain (build_cookie ({ name: 'mint_cookies' }, res1.rt as string));
await check_token (res1.at as string, 'access_token');
expect (res1.data.expires_in)
.toEqual (expires_seconds);
await check_token (res1.rt as string, 'refresh_token');
expect (res1.data.refresh_expires_in)
.toEqual (refresh_expires_seconds);
// get refresh token
const resp2 = await get ({ authorization: `Bearer ${res1.rt}` });
expect (resp2.statusCode)
.toEqual (200);
const res2 = check_headers (resp2);
expect (res2.data.token_type)
.toEqual ('bearer');
expect (resp2.headers['set-cookie'])
.toContain (build_cookie ({ name: 'cookie_jar' }, res2.at as string));
expect (resp2.headers['set-cookie'])
.toContain (build_cookie ({ name: 'mint_cookies' }, res2.rt as string));
await check_token (res2.at as string, 'access_token');
expect (res2.data.expires_in)
.toEqual (expires_seconds);
expect (res2.at).not.toEqual (res1.at);
await check_token (res2.rt as string, 'refresh_token');
expect (res2.data.refresh_expires_in)
.toEqual (refresh_expires_seconds);
expect (res2.rt).not.toEqual (res1.rt);
});
it ('should return the correct denial message', async () => {
const resp = await get ({ authorization: 'Basic bar:baz' });
expect (resp.statusCode)
.toEqual (401);
const res = check_headers (resp);
expect (res.data)
.toEqual ({ error: 'invalid_client' });
});
it ('should allow base64 login', async () => {
const resp1 = await get ({ authorization: `Basic ${to_b64 ('foo:bar')}` });
expect (resp1.statusCode)
.toEqual (200);
const res1 = check_headers (resp1);
expect (res1.data.token_type)
.toEqual ('bearer');
expect (resp1.headers['set-cookie'])
.toContain (build_cookie ({ name: 'cookie_jar' }, res1.at as string));
expect (resp1.headers['set-cookie'])
.toContain (build_cookie ({ name: 'mint_cookies' }, res1.rt as string));
await check_token (res1.at as string, 'access_token');
expect (res1.data.expires_in)
.toEqual (expires_seconds);
await check_token (res1.rt as string, 'refresh_token');
expect (res1.data.refresh_expires_in)
.toEqual (refresh_expires_seconds);
});
it ('should allow body login', async () => {
const resp1 = await get (
// eslint-disable-next-line @typescript-eslint/naming-convention
{ 'Content-Type': 'application/json' },
JSON.stringify ({ username: 'foo', password: 'bar' })
);
expect (resp1.statusCode)
.toEqual (200);
const res1 = check_headers (resp1);
expect (res1.data.token_type)
.toEqual ('bearer');
expect (resp1.headers['set-cookie'])
.toContain (build_cookie ({ name: 'cookie_jar' }, res1.at as string));
expect (resp1.headers['set-cookie'])
.toContain (build_cookie ({ name: 'mint_cookies' }, res1.rt as string));
await check_token (res1.at as string, 'access_token');
expect (res1.data.expires_in)
.toEqual (expires_seconds);
await check_token (res1.rt as string, 'refresh_token');
expect (res1.data.refresh_expires_in)
.toEqual (refresh_expires_seconds);
});
it ('should reject invalid requests', async () => {
const resp1 = await get ();
expect (resp1.statusCode)
.toEqual (400);
const res1 = check_headers (resp1);
expect (res1.data)
.toEqual ({
error: 'invalid_request',
error_description: 'unknown authorization type'
});
const resp2a = await get ({ authorization: 'Basic foo:bar' });
const res2a = check_headers (resp2a);
const resp2b = await get (
{ authorization: `Bearer ${res2a.at}` }
);
expect (resp2b.statusCode)
.toEqual (400);
const res2 = check_headers (resp2b);
expect (res2.data)
.toEqual ({
error: 'invalid_request',
error_description: 'invalid bearer type'
});
});
it ('should reject an invalid token', async () => {
const resp1 = await get ({ authorization: 'Basic foo:bar' });
const res1 = check_headers (resp1);
const resp2 = await get (
{ authorization: `Bearer ${modify_signature (res1.at)}` }
);
expect (resp2.statusCode)
.toEqual (401);
const res2 = check_headers (resp2);
expect (res2.data)
.toEqual ({ error: 'invalid_client' });
});
it ('should process part token', async () => {
const resp1 = await get ({ authorization: 'Basic part:bar' });
expect (resp1.statusCode)
.toEqual (200);
const res1 = check_headers (resp1);
expect (res1.data.token_type)
.toEqual ('bearer');
expect (res1.data.expires_in)
.toEqual (part_expires_seconds);
await check_token (res1.data.part_token as string, 'part_token');
const resp2 = await get (
{ authorization: `Bearer ${res1.data.part_token}` },
'letmein'
);
expect (resp2.statusCode)
.toEqual (200);
const res2 = check_headers (resp2);
expect (res2.data.token_type)
.toEqual ('bearer');
expect (resp2.headers['set-cookie'])
.toContain (build_cookie ({ name: 'cookie_jar' }, res2.at as string));
expect (resp2.headers['set-cookie'])
.toContain (build_cookie ({ name: 'mint_cookies' }, res2.rt as string));
await check_token (res2.at as string, 'access_token');
expect (res2.data.expires_in)
.toEqual (expires_seconds);
expect (res2.at).not.toEqual (res1.at);
await check_token (res2.rt as string, 'refresh_token');
expect (res2.data.refresh_expires_in)
.toEqual (refresh_expires_seconds);
expect (res2.rt).not.toEqual (res1.rt);
});
it ('should do immediate redirect', async () => {
const resp1 = await get ({ authorization: 'Basic red:irect' });
expect (resp1.statusCode)
.toEqual (302);
expect (resp1.headers.location)
.toEqual ('/redirected');
const signature = extract_cookie (
'cookie_jar',
(resp1.headers['set-cookie'] || []).join ('\n')
);
await check_token (signature, 'access_token');
});
it ('should handle any authorization type', async () => {
const resp = await get ({ authorization: 'Foo asdefg' });
expect (resp.statusCode)
.toEqual (400);
expect (JSON.parse (resp.body as string))
.toEqual ({
error: 'invalid_request',
error_description: 'unknown authorization type'
});
});
it ('should not set content-type when leave-open is specified', async () => {
const resp1 = await get ({ authorization: 'Basic leave:open' });
expect (resp1.statusCode)
.toEqual (200);
expect (resp1.headers['content-type'])
.toEqual ('text/plain');
expect (resp1.body)
.toEqual ('custom response, true');
const signature = extract_cookie (
'cookie_jar',
(resp1.headers['set-cookie'] || []).join ('\n')
);
expect (signature).not.toEqual ('');
await check_token (signature, 'access_token');
});
it ('should disallow access and refresh cookies with the same name', () => {
expect (() => {
create_auth_handler (() => Promise.resolve (), {
cookie: { name: 'foo' },
refresh_cookie: { name: 'foo' }
});
})
.toThrowError ('access and refresh cookies cannot have the same name');
});
});

162
test/spec/Authority.ts Normal file
View File

@ -0,0 +1,162 @@
/*
* Copyright (C) Sapphirecode - All Rights Reserved
* This file is part of Auth-Server-Helper which is released under MIT.
* See file 'LICENSE' for full license details.
* Created by Timo Hocker <timo@scode.ovh>, December 2020
*/
import auth from '../../lib/Authority';
import bl from '../../lib/Blacklist';
import {
clock_finalize,
clock_setup,
modify_signature
} from '../Helper';
// eslint-disable-next-line max-lines-per-function
describe ('authority', () => {
beforeEach (() => {
clock_setup ();
});
afterEach (() => {
clock_finalize ();
});
it ('should create an access token', async () => {
const token = await auth.sign ('access_token', 60);
jasmine.clock ()
.tick (30000);
const res = await auth.verify (token.signature);
expect (res.authorized)
.toBeTrue ();
expect (res.valid)
.toBeTrue ();
expect (res.type)
.toEqual ('access_token');
expect (res.next_module)
.toBeUndefined ();
expect (res.id)
.toEqual (token.id);
expect (res.error)
.toBeUndefined ();
});
it ('should create a refresh token', async () => {
const token = await auth.sign ('refresh_token', 600);
jasmine.clock ()
.tick (30000);
const res = await auth.verify (token.signature);
expect (res.authorized)
.toBeFalse ();
expect (res.valid)
.toBeTrue ();
expect (res.type)
.toEqual ('refresh_token');
expect (res.next_module)
.toBeUndefined ();
expect (res.id)
.toEqual (token.id);
expect (res.error)
.toBeUndefined ();
});
it ('should create a part token', async () => {
const token = await auth.sign ('part_token', 60, { next_module: '2fa' });
jasmine.clock ()
.tick (30000);
const res = await auth.verify (token.signature);
expect (res.authorized)
.toBeFalse ();
expect (res.valid)
.toBeTrue ();
expect (res.type)
.toEqual ('part_token');
expect (res.next_module)
.toEqual ('2fa');
expect (res.id)
.toEqual (token.id);
expect (res.error)
.toBeUndefined ();
});
it ('should reject an invalid access token', async () => {
const token = await auth.sign ('access_token', 60);
token.signature = modify_signature (token.signature);
jasmine.clock ()
.tick (30000);
const res = await auth.verify (token.signature);
expect (res.authorized)
.toBeFalse ();
expect (res.valid)
.toBeFalse ();
expect (res.type)
.toEqual ('none');
expect (res.next_module)
.toBeUndefined ();
expect (res.id)
.toEqual ('');
expect (res.error)
.toEqual ('invalid signature');
});
it ('should reject blacklisted access token', async () => {
const token = await auth.sign ('access_token', 60);
jasmine.clock ()
.tick (30000);
await bl.add_signature (token.id);
const res = await auth.verify (token.signature);
expect (res.authorized)
.toBeFalse ();
expect (res.valid)
.toBeFalse ();
expect (res.type)
.toEqual ('access_token');
expect (res.next_module)
.toBeUndefined ();
expect (res.id)
.toEqual (token.id);
expect (res.error)
.toEqual ('blacklisted');
});
it ('should reject an invalid refresh token', async () => {
const token = await auth.sign ('refresh_token', 600);
token.signature = modify_signature (token.signature);
jasmine.clock ()
.tick (30000);
const res = await auth.verify (token.signature);
expect (res.authorized)
.toBeFalse ();
expect (res.valid)
.toBeFalse ();
expect (res.type)
.toEqual ('none');
expect (res.next_module)
.toBeUndefined ();
expect (res.id)
.toEqual ('');
expect (res.error)
.toEqual ('invalid signature');
});
it ('should reject a blacklisted refresh token', async () => {
const token = await auth.sign ('refresh_token', 600);
jasmine.clock ()
.tick (30000);
await bl.add_signature (token.id);
const res = await auth.verify (token.signature);
expect (res.authorized)
.toBeFalse ();
expect (res.valid)
.toBeFalse ();
expect (res.type)
.toEqual ('refresh_token');
expect (res.next_module)
.toBeUndefined ();
expect (res.id)
.toEqual (token.id);
expect (res.error)
.toEqual ('blacklisted');
});
});

111
test/spec/Blacklist.ts Normal file
View File

@ -0,0 +1,111 @@
/*
* Copyright (C) Sapphirecode - All Rights Reserved
* This file is part of Auth-Server-Helper which is released under MIT.
* See file 'LICENSE' for full license details.
* Created by Timo Hocker <timo@scode.ovh>, December 2020
*/
import blacklist, { Blacklist } from '../../lib/Blacklist';
import { generate_token_id } from '../../lib/token_id';
import { clock_finalize, clock_setup } from '../Helper';
// eslint-disable-next-line max-lines-per-function
describe ('blacklist', () => {
const token1 = generate_token_id (new Date (Date.now () + 3600000));
const token2 = generate_token_id (new Date (Date.now () + 3600000));
const token3 = generate_token_id (new Date (Date.now () + 3600000));
beforeAll (() => {
clock_setup ();
});
afterAll (() => {
clock_finalize ();
});
it ('should validate any string', async () => {
expect (await blacklist.is_valid (token1))
.toBeTrue ();
expect (await blacklist.is_valid (token2))
.toBeTrue ();
expect (await blacklist.is_valid (token3))
.toBeTrue ();
});
it ('should blacklist strings', async () => {
await blacklist.add_signature (token1);
await blacklist.add_signature (token2);
expect (await blacklist.is_valid (token1))
.toBeFalse ();
expect (await blacklist.is_valid (token2))
.toBeFalse ();
expect (await blacklist.is_valid (token3))
.toBeTrue ();
});
it ('should remove one string', async () => {
await blacklist.remove_signature (token1);
expect (await blacklist.is_valid (token1))
.toBeTrue ();
expect (await blacklist.is_valid (token2))
.toBeFalse ();
expect (await blacklist.is_valid (token3))
.toBeTrue ();
});
it ('should clear after time', async () => {
jasmine.clock ()
.tick (5000);
await blacklist.add_signature (token3);
await blacklist.clear (Date.now () - 100);
expect (await blacklist.is_valid (token1))
.toBeTrue ();
expect (await blacklist.is_valid (token2))
.toBeTrue ();
expect (await blacklist.is_valid (token3))
.toBeFalse ();
});
it ('should clear all', async () => {
await blacklist.add_signature (token1);
await blacklist.add_signature (token2);
await blacklist.add_signature (token3);
expect (await blacklist.is_valid (token1))
.toBeFalse ();
expect (await blacklist.is_valid (token2))
.toBeFalse ();
expect (await blacklist.is_valid (token3))
.toBeFalse ();
await blacklist.clear ();
expect (await blacklist.is_valid (token1))
.toBeTrue ();
expect (await blacklist.is_valid (token2))
.toBeTrue ();
expect (await blacklist.is_valid (token3))
.toBeTrue ();
});
it ('should export and import data', async () => {
const time = new Date;
const token = generate_token_id (time);
await blacklist.add_signature (token);
// eslint-disable-next-line dot-notation
expect (blacklist['_signatures'])
.toEqual ([
{
token_id: token,
iat: time.getTime (),
valid_until: time
}
]);
const exp = blacklist.export_blacklist ();
expect (exp)
.toEqual ([ { token_id: token, iat: time.getTime () } ]);
const bl2 = (new Blacklist);
bl2.import_blacklist (exp);
// eslint-disable-next-line dot-notation
expect (bl2['_signatures'])
// eslint-disable-next-line dot-notation
.toEqual (blacklist['_signatures']);
});
});

226
test/spec/Gateway.ts Normal file
View File

@ -0,0 +1,226 @@
/*
* Copyright (C) Sapphirecode - All Rights Reserved
* This file is part of Auth-Server-Helper which is released under MIT.
* See file 'LICENSE' for full license details.
* Created by Timo Hocker <timo@scode.ovh>, January 2021
*/
import http from 'http';
import { GatewayClass, create_gateway } from '../../lib/index';
import authority from '../../lib/Authority';
import blacklist from '../../lib/Blacklist';
import { clock_finalize, clock_setup, get } from '../Helper';
// eslint-disable-next-line max-lines-per-function
describe ('gateway', () => {
let server: http.Server|null = null;
beforeAll (() => {
clock_setup ();
const g = new GatewayClass ({
redirect_url: 'http://localhost/auth',
cookie: { name: 'cookie_jar' },
refresh_cookie: { name: 'mint_cookies' },
refresh_settings: {
access_token_expires_in: 600,
include_refresh_token: true,
refresh_token_expires_in: 3600
}
});
server = http.createServer ((req, res) => {
const passed_handler = () => {
if (typeof req.url !== 'undefined') {
if (req.url.endsWith ('logout'))
g.logout (req);
}
res.writeHead (200);
const data = {
...g.get_info (req),
foo: g.has_permission (req, 'foo'),
bar: g.has_permission (req, 'bar')
};
res.end (JSON.stringify (data));
};
g.process_request (req, res, passed_handler);
});
server.listen (3000);
});
afterAll (() => {
if (server === null)
throw new Error ('server is null');
server.close ();
clock_finalize ();
});
it ('should redirect any unauthorized request', async () => {
const resp = await get ();
expect (resp.statusCode)
.toEqual (302);
expect (resp.headers.location)
.toEqual ('http://localhost/auth');
});
it ('should allow a valid access token', async () => {
const token = await authority.sign ('access_token', 60);
const resp = await get ({ authorization: `Bearer ${token.signature}` });
expect (resp.statusCode)
.toEqual (200);
expect (JSON.parse (resp.body as string).token_id)
.toEqual (token.id);
});
it ('should allow a valid access token using cookies', async () => {
const token = await authority.sign ('access_token', 60);
const resp = await get (
{ cookie: `foo=bar;cookie_jar=${token.signature};asd=efg` }
);
expect (resp.statusCode)
.toEqual (200);
expect (JSON.parse (resp.body as string).token_id)
.toEqual (token.id);
});
it ('should automatically return new tokens', async () => {
const token = await authority.sign ('access_token', 60, { data: 'foobar' });
const refresh = await authority.sign (
'refresh_token',
3600,
{ data: 'foobar' }
);
jasmine.clock ()
.tick (70000);
const resp = await get (
// eslint-disable-next-line max-len
{ cookie: `foo=bar;cookie_jar=${token.signature};asd=efg;mint_cookies=${refresh.signature}` }
);
expect (resp.statusCode)
.toEqual (200);
expect (JSON.parse (resp.body as string).token_id)
.not
.toEqual (token.id);
expect (JSON.parse (resp.body as string).token_data)
.toEqual ('foobar');
});
it ('should correctly deliver token data', async () => {
const token = await authority.sign ('access_token', 60, { data: 'foobar' });
const resp = await get ({ authorization: `Bearer ${token.signature}` });
expect (resp.statusCode)
.toEqual (200);
const body = JSON.parse (resp.body as string);
expect (body.token_id)
.toEqual (token.id);
expect (body.token_data)
.toEqual ('foobar');
expect (body.permissions)
.toEqual ([]);
});
it ('should reject an outdated access token', async () => {
const token = await authority.sign ('access_token', 60);
jasmine.clock ()
.tick (70000);
const resp = await get ({ authorization: `Bearer ${token.signature}` });
expect (resp.statusCode)
.toEqual (302);
expect (resp.headers.location)
.toEqual ('http://localhost/auth');
});
it ('should reject a blacklisted access token', async () => {
const token = await authority.sign ('access_token', 60);
await blacklist.add_signature (token.id);
const resp = await get ({ authorization: `Bearer ${token.signature}` });
expect (resp.statusCode)
.toEqual (302);
expect (resp.headers.location)
.toEqual ('http://localhost/auth');
});
it ('should reject any refresh_token', async () => {
const token = await authority.sign ('refresh_token', 60);
const resp = await get ({ authorization: `Bearer ${token.signature}` });
expect (resp.statusCode)
.toEqual (302);
expect (resp.headers.location)
.toEqual ('http://localhost/auth');
});
it ('should reject any part_token', async () => {
const token = await authority.sign ('part_token', 60);
const resp = await get ({ authorization: `Bearer ${token.signature}` });
expect (resp.statusCode)
.toEqual (302);
expect (resp.headers.location)
.toEqual ('http://localhost/auth');
});
it ('should reject any noname token', async () => {
const token = await authority.sign ('none', 60);
const resp = await get ({ authorization: `Bearer ${token.signature}` });
expect (resp.statusCode)
.toEqual (302);
expect (resp.headers.location)
.toEqual ('http://localhost/auth');
});
it ('should reject non-bearer auth', async () => {
const resp = await get ({ authorization: 'Basic foo:bar' });
expect (resp.statusCode)
.toEqual (302);
expect (resp.headers.location)
.toEqual ('http://localhost/auth');
});
it ('should disallow access and refresh cookies with the same name', () => {
expect (() => {
create_gateway ({
cookie: { name: 'foo' },
refresh_cookie: { name: 'foo' }
});
})
.toThrowError ('access and refresh cookies cannot have the same name');
});
it ('should logout all tokens', async () => {
const token = await authority.sign ('access_token', 60);
const refresh = await authority.sign ('refresh_token', 3600);
const resp = await get (
// eslint-disable-next-line max-len
{ cookie: `foo=bar;cookie_jar=${token.signature};asd=efg;mint_cookies=${refresh.signature}` },
null,
'/logout'
);
expect (resp.statusCode)
.toEqual (200);
const blacklisted = blacklist.export_blacklist ()
.map ((v) => v.token_id);
expect (blacklisted)
.toContain (token.id);
expect (blacklisted)
.toContain (refresh.id);
});
it ('should correctly check permissions', async () => {
const token = await authority.sign (
'access_token',
60,
{ permissions: [ 'foo' ] }
);
const resp = await get ({ authorization: `Bearer ${token.signature}` });
expect (resp.statusCode)
.toEqual (200);
expect (JSON.parse (resp.body as string))
.toEqual ({
token_id: token.id,
permissions: [ 'foo' ],
foo: true,
bar: false
});
});
});

194
test/spec/KeyStore.ts Normal file
View File

@ -0,0 +1,194 @@
/*
* Copyright (C) Sapphirecode - All Rights Reserved
* This file is part of Auth-Server-Helper which is released under MIT.
* See file 'LICENSE' for full license details.
* Created by Timo Hocker <timo@scode.ovh>, December 2020
*/
import ks, { KeyStore } from '../../lib/KeyStore';
import { clock_finalize, clock_setup } from '../Helper';
const frame = 3600;
/* eslint-disable-next-line max-lines-per-function */
describe ('key store', () => {
beforeAll (() => {
clock_setup ();
});
afterAll (() => {
clock_finalize ();
});
const keys: {key: string, sign: string, iat: number}[] = [];
it ('should generate a new key', async () => {
const iat = (new Date)
.getTime () / 1000;
const duration = 10 * frame;
const key = await ks.get_sign_key (iat, duration);
const sign = await ks.get_key (iat);
expect (typeof key)
.toEqual ('string');
expect (typeof sign)
.toEqual ('string');
keys.push ({ iat, key, sign });
});
it ('should return the generated key', async () => {
const key = await ks.get_sign_key (keys[0].iat, 1);
expect (key)
.toEqual (keys[0].key);
const sign = await ks.get_key (keys[0].iat);
expect (sign)
.toEqual (keys[0].sign);
});
it ('should return the same key on a different time', async () => {
const key = await ks.get_sign_key (keys[0].iat + (frame / 2), 1);
expect (key)
.toEqual (keys[0].key);
const sign = await ks.get_key (keys[0].iat + (frame / 2));
expect (sign)
.toEqual (keys[0].sign);
});
it ('should generate a new key after time frame is over', async () => {
jasmine.clock ()
.tick (frame * 1000);
const iat = (new Date)
.getTime () / 1000;
const duration = 10 * frame;
const key = await ks.get_sign_key (iat, duration);
const sign = await ks.get_key (iat);
expect (typeof key)
.toEqual ('string');
expect (key).not.toEqual (keys[0].key);
expect (sign).not.toEqual (keys[0].sign);
keys.push ({ iat, key, sign });
});
it ('should return both keys, but not the first sign key', async () => {
const sign = await ks.get_key (keys[0].iat);
expect (sign)
.toEqual (keys[0].sign);
await expectAsync (ks.get_sign_key (keys[0].iat, 1))
.toBeRejectedWithError ('cannot access already expired keys');
const k2 = await ks.get_sign_key (keys[1].iat, 1);
const s2 = await ks.get_key (keys[1].iat);
expect (k2)
.toEqual (keys[1].key);
expect (s2)
.toEqual (keys[1].sign);
});
it ('should throw on non existing key', async () => {
await expectAsync (ks.get_key (keys[1].iat + frame))
.toBeRejectedWithError ('key could not be found');
});
it ('should delete a key after it expires', async () => {
// go to 10 frames + 1ms after key creation
jasmine.clock ()
.tick ((frame * 9e3) + 1);
// eslint-disable-next-line dot-notation
ks['garbage_collect'] ();
await expectAsync (ks.get_key (keys[0].iat))
.toBeRejectedWithError ('key could not be found');
});
it (
'should still retrieve the second key, but not its sign key',
async () => {
await expectAsync (ks.get_sign_key (keys[1].iat, 1))
.toBeRejectedWithError ('cannot access already expired keys');
const sign = await ks.get_key (keys[1].iat);
expect (sign)
.toEqual (keys[1].sign);
}
);
it ('should reject key generation of expired keys', async () => {
const iat = ((new Date)
.getTime () / 1000) - 2;
const duration = 5;
await expectAsync (ks.get_sign_key (iat, duration))
.toBeRejectedWithError ('cannot access already expired keys');
});
it ('key should live as long as the longest created token', async () => {
jasmine.clock ()
.tick (frame * 10e3);
const iat = (new Date)
.getTime () / 1000;
const duration1 = frame;
const duration2 = frame * 10;
const key1 = await ks.get_sign_key (iat, duration1);
const step = 0.9 * frame;
jasmine.clock ()
.tick (step * 1000);
const key2 = await ks.get_sign_key (iat + step, duration2);
const sign = await ks.get_key (iat);
expect (key1)
.toEqual (key2);
jasmine.clock ()
.tick (5000 * frame);
const signv = await ks.get_key (iat + step);
expect (signv)
.toEqual (sign);
});
it ('should not allow invalid expiry times', async () => {
await expectAsync (ks.get_sign_key (0, 0))
.toBeRejectedWithError ('cannot create infinitely valid key');
await expectAsync (ks.get_sign_key (0, -1))
.toBeRejectedWithError ('cannot create infinitely valid key');
});
it ('should export and import all keys', async () => {
const iat = (new Date)
.getTime () / 1000;
const sign = await ks.get_sign_key (iat, frame);
const ver = await ks.get_key (iat);
const exp = ks.export_verification_data ();
// eslint-disable-next-line dot-notation
expect (Object.keys (ks['_keys']))
.toEqual (exp.map ((v) => v.index));
const ks2 = (new KeyStore);
expect (ks2.instance_id).not.toEqual (ks.instance_id);
ks2.import_verification_data (exp);
// eslint-disable-next-line dot-notation
expect (Object.keys (ks2['_keys']))
.toEqual (exp.map ((v) => v.index));
const sign2 = await ks2.get_sign_key (iat, frame);
const ver2 = await ks2.get_key (iat);
expect (sign).not.toEqual (sign2);
expect (ver).not.toEqual (ver2);
await expectAsync (ks2.get_sign_key (iat, 60, ks.instance_id))
.toBeRejectedWithError ('cannot access already expired keys');
expect (await ks2.get_key (iat, ks.instance_id))
.toEqual (ver);
});
it ('should disallow importing to itself', () => {
const exp = ks.export_verification_data ();
expect (() => ks.import_verification_data (exp))
.toThrowError ('cannot import to the same instance');
});
it ('should clear all', () => {
// eslint-disable-next-line dot-notation
expect (Object.keys (ks['_keys']).length)
.toBeGreaterThan (0);
const instance = ks.instance_id;
ks.reset_instance ();
// eslint-disable-next-line dot-notation
expect (Object.keys (ks['_keys']).length)
.toEqual (0);
expect (instance).not.toEqual (ks.instance_id);
});
});

110
test/spec/Redis.ts Normal file
View File

@ -0,0 +1,110 @@
/*
* Copyright (C) Sapphirecode - All Rights Reserved
* This file is part of Auth-Server-Helper which is released under MIT.
* See file 'LICENSE' for full license details.
* Created by Timo Hocker <timo@scode.ovh>, August 2022
*/
/* eslint-disable dot-notation */
import { blacklist } from '../../lib';
import ks from '../../lib/KeyStore';
import { Redis } from '../../lib/Redis';
import { generate_token_id } from '../../lib/token_id';
import { clock_finalize, clock_setup } from '../Helper';
const frame = 3600;
const redis_url = process.env.TEST_REDIS_URL || 'redis://localhost';
const redis = new Redis;
redis.connect (redis_url);
// eslint-disable-next-line max-lines-per-function
describe ('redis', () => {
const token1 = generate_token_id (new Date (Date.now () + 3600000));
const token2 = generate_token_id (new Date (Date.now () + 3600000));
const token3 = generate_token_id (new Date (Date.now () + 3600000));
beforeAll (async () => {
ks.reset_instance ();
ks.sync_redis (redis_url);
await blacklist.clear ();
blacklist.sync_redis (redis_url);
clock_setup ();
});
let iat1 = 0;
let iat2 = 0;
let k1 = '';
let k2 = '';
let i1 = '';
let i2 = '';
afterAll (() => clock_finalize ());
it ('should generate two keys', async () => {
iat1 = (new Date)
.getTime () / 1000;
await ks.get_sign_key (iat1, frame);
k1 = await ks.get_key (iat1);
jasmine.clock ()
.tick (frame * 1000);
iat2 = (new Date)
.getTime () / 1000;
await ks.get_sign_key (iat2, frame);
k2 = await ks.get_key (iat2);
// eslint-disable-next-line dot-notation
i1 = ks['get_index'] (iat1);
// eslint-disable-next-line dot-notation
i2 = ks['get_index'] (iat2);
});
it ('should have two keys in redis', async () => {
expect (JSON.parse (await redis['_redis']
?.get (`keystore_${i1}`) as string).key)
.toEqual (k1);
expect (JSON.parse (await redis['_redis']
?.get (`keystore_${i2}`) as string).key)
.toEqual (k2);
});
it ('should read two keys with a new instance', async () => {
const old_instance = ks.instance_id;
ks.reset_instance ();
expectAsync (ks.get_key (iat1, old_instance))
.toBeRejectedWithError ('key could not be found');
expectAsync (ks.get_key (iat1, old_instance))
.toBeRejectedWithError ('key could not be found');
ks.sync_redis (redis_url);
expect (await ks.get_key (iat1, old_instance))
.toEqual (k1);
expect (await ks.get_key (iat2, old_instance))
.toEqual (k2);
});
it ('should add two keys to the blacklist', async () => {
await blacklist.add_signature (token1);
await blacklist.add_signature (token2);
});
it ('should have two keys in redis blacklist', async () => {
expect ((await redis['_redis']?.exists (`blacklist_${token1}`)) === 1)
.toBeTrue ();
expect ((await redis['_redis']?.exists (`blacklist_${token2}`)) === 1)
.toBeTrue ();
expect ((await redis['_redis']?.exists (`blacklist_${token3}`)) === 1)
.toBeFalse ();
});
it ('should read keys from redis', async () => {
blacklist['_signatures'].splice (0, blacklist['_signatures'].length);
expect (await blacklist.is_valid (token1))
.toBeFalse ();
expect (await blacklist.is_valid (token2))
.toBeFalse ();
expect (await blacklist.is_valid (token3))
.toBeTrue ();
});
});

130
test/spec/cookie.ts Normal file
View File

@ -0,0 +1,130 @@
/*
* Copyright (C) Sapphirecode - All Rights Reserved
* This file is part of Auth-Server-Helper which is released under MIT.
* See file 'LICENSE' for full license details.
* Created by Timo Hocker <timo@scode.ovh>, January 2022
*/
import { build_cookie, CookieSettings, extract_cookie } from '../../lib/cookie';
interface CreateCookie {
settings: CookieSettings
value: string
result: string
}
const create_cookie_pairs: CreateCookie[] = [
{
settings: { name: 'foo' },
value: 'bar',
result: 'foo=bar; Secure; HttpOnly; SameSite=Strict'
},
{
settings: { name: 'foäöüo' },
value: 'baäöür',
result: 'foäöüo=baäöür; Secure; HttpOnly; SameSite=Strict'
},
{
settings: {
name: 'foo',
secure: true,
http_only: false,
same_site: null
},
value: 'bar',
result: 'foo=bar; Secure'
},
{
settings: {
name: 'foo',
secure: false,
http_only: true,
same_site: null
},
value: 'bar',
result: 'foo=bar; HttpOnly'
},
{
settings: {
name: 'foo',
secure: false,
http_only: false,
same_site: 'Lax'
},
value: 'bar',
result: 'foo=bar; SameSite=Lax'
},
{
settings: {
name: 'foo',
secure: false,
http_only: false,
same_site: null,
expires: 'Tomorrow'
},
value: 'bar',
result: 'foo=bar; Expires=Tomorrow'
},
{
settings: {
name: 'foo',
secure: false,
http_only: false,
same_site: null,
max_age: 600
},
value: 'bar',
result: 'foo=bar; Max-Age=600'
},
{
settings: {
name: 'foo',
secure: false,
http_only: false,
same_site: null,
domain: 'example.com'
},
value: 'bar',
result: 'foo=bar; Domain=example.com'
},
{
settings: {
name: 'foo',
secure: false,
http_only: false,
same_site: null,
path: '/test'
},
value: 'bar',
result: 'foo=bar; Path=/test'
}
];
const parse_cookie_pairs = [
{
header: 'foo=bar; Secure; HttpOnly; SameSite=Strict',
name: 'foo',
value: 'bar'
},
{
header: '134=567;foäöüo=baäöür;tesT=123',
name: 'foäöüo',
value: 'baäöür'
}
];
describe ('cookie', () => {
it ('should create a cookie', () => {
for (const pair of create_cookie_pairs) {
expect (build_cookie (pair.settings, pair.value))
.toEqual (pair.result);
}
});
it ('should parse a cookie', () => {
for (const pair of parse_cookie_pairs) {
expect (extract_cookie (pair.name, pair.header))
.toEqual (pair.value);
}
});
});

22
test/spec/token_id.ts Normal file
View File

@ -0,0 +1,22 @@
import { generate_token_id, parse_token_id } from '../../lib/token_id';
import { clock_finalize, clock_setup } from '../Helper';
describe ('token_id', () => {
beforeAll (() => {
clock_setup ();
});
afterAll (() => {
clock_finalize ();
});
it ('should always generate valid tokens', () => {
for (let i = 0; i < 1000; i++) {
const date = new Date;
const token_id = generate_token_id (new Date);
const parsed = parse_token_id (token_id);
expect (parsed.valid_until)
.toEqual (date);
}
});
});

16
tsconfig.json Normal file
View File

@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"outDir": "./dist",
"strict": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"declaration": true,
"sourceMap": true
},
"include": [
"lib/**/*.ts",
"test/**/*.ts"
]
}

File diff suppressed because it is too large Load Diff

7316
yarn.lock

File diff suppressed because it is too large Load Diff