This commit is contained in:
parent
fe037d43d3
commit
210696dda0
@ -1 +1,2 @@
|
||||
/dist/
|
||||
*.d.ts
|
||||
|
22
.eslintrc.js
22
.eslintrc.js
@ -1,22 +1,24 @@
|
||||
/*
|
||||
* Copyright (C) Sapphirecode - All Rights Reserved
|
||||
* This file is part of auth-server-helper which is released under MIT.
|
||||
* 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>, May 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: [ '@sapphirecode' ],
|
||||
extends: [
|
||||
'@sapphirecode'
|
||||
],
|
||||
globals: {
|
||||
Atomics: 'readonly',
|
||||
Atomics: 'readonly',
|
||||
SharedArrayBuffer: 'readonly'
|
||||
},
|
||||
parserOptions: { ecmaVersion: 2018 }
|
||||
};
|
||||
parserOptions: {
|
||||
ecmaVersion: 2018
|
||||
}
|
||||
}
|
||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -2,6 +2,3 @@
|
||||
/dist/
|
||||
/.nyc_output/
|
||||
/coverage/
|
||||
/db.sqlite
|
||||
# stryker temp files
|
||||
.stryker-tmp
|
||||
|
@ -4,5 +4,5 @@
|
||||
"author": "Timo Hocker",
|
||||
"company": "Sapphirecode",
|
||||
"email": "timo@scode.ovh",
|
||||
"software": "auth-server-helper"
|
||||
"software": "Auth-Server-Helper"
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
# Changelog
|
||||
|
||||
## 1.1.0
|
||||
|
||||
add user_id to res.connection, so request handlers can access the current user
|
||||
|
||||
## 1.0.0
|
||||
|
||||
initial release
|
53
README.md
53
README.md
@ -1,63 +1,22 @@
|
||||
# @sapphirecode/auth-server-helper
|
||||
# auth-server-helper
|
||||
|
||||
version: 1.1.x
|
||||
version: 0.0.0
|
||||
|
||||
authentication middleware for express
|
||||
undefined
|
||||
|
||||
## Installation
|
||||
|
||||
npm:
|
||||
|
||||
> npm i --save @sapphirecode/auth-server-helper
|
||||
> npm i --save auth-server-helper
|
||||
|
||||
yarn:
|
||||
|
||||
> yarn add @sapphirecode/auth-server-helper
|
||||
> yarn add auth-server-helper
|
||||
|
||||
## Usage
|
||||
|
||||
```js
|
||||
const auth = require('@sapphirecode/auth-server-helper');
|
||||
const password_helper = require('@sapphirecode/password_helper');
|
||||
|
||||
const users = {
|
||||
foo: {
|
||||
id: 0
|
||||
password: await password_helper.hash('bar'),
|
||||
salt: '123'
|
||||
}
|
||||
}
|
||||
|
||||
// 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];
|
||||
}));
|
||||
|
||||
```
|
||||
|
||||
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 id of the logged in user will be available in `req.connection.user_id` in
|
||||
all of the following request handlers.
|
||||
|
||||
### 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.
|
||||
|
||||
```js
|
||||
auth(..., [/no-auth/, {regex: '/no-auth-post/', method: 'POST'}]);
|
||||
```
|
||||
TODO: Add usage
|
||||
|
||||
## License
|
||||
|
||||
|
206
index.js
206
index.js
@ -1,206 +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>, May 2020
|
||||
*/
|
||||
|
||||
// @ts-nocheck
|
||||
/* eslint-disable no-magic-numbers */
|
||||
|
||||
'use strict';
|
||||
|
||||
const password_helper = require ('@sapphirecode/password-helper');
|
||||
const crypto = require ('@sapphirecode/crypto-helper');
|
||||
const consts = require ('@sapphirecode/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
|
||||
* @param {any} req request object
|
||||
* @returns {Promise<string>} session key if successful
|
||||
*/
|
||||
async function authenticate (user, password, req) {
|
||||
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;
|
||||
|
||||
req.connection.user_id = user_entry.id;
|
||||
|
||||
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} req request object
|
||||
* @param {any} res response object
|
||||
* @param {any} next next handler
|
||||
* @returns {Promise<boolean>} true if handler authenticated
|
||||
*/
|
||||
// eslint-disable-next-line max-len, max-params
|
||||
async function request_handler_authenticate (session, user, key, req, 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, req);
|
||||
|
||||
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
|
||||
);
|
||||
req.connection.user_id = jwt.id;
|
||||
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, req, res, next))
|
||||
return;
|
||||
|
||||
res.status (consts.http.status_forbidden);
|
||||
res.end ();
|
||||
}
|
||||
|
||||
module.exports = init;
|
24
lib/.eslintrc.js
Normal file
24
lib/.eslintrc.js
Normal file
@ -0,0 +1,24 @@
|
||||
/*
|
||||
* 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
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
env: {
|
||||
commonjs: true,
|
||||
es6: true,
|
||||
node: true
|
||||
},
|
||||
extends: [
|
||||
'@sapphirecode/eslint-config-ts'
|
||||
],
|
||||
globals: {
|
||||
Atomics: 'readonly',
|
||||
SharedArrayBuffer: 'readonly'
|
||||
},
|
||||
parserOptions: {
|
||||
ecmaVersion: 2018
|
||||
}
|
||||
}
|
@ -1,55 +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>, May 2020
|
||||
*/
|
||||
|
||||
/* eslint-disable no-magic-numbers */
|
||||
// @ts-nocheck
|
||||
'use strict';
|
||||
|
||||
const express = require ('express');
|
||||
const auth = require ('./index');
|
||||
const consts = require ('@sapphirecode/consts');
|
||||
const crypto = require ('@sapphirecode/crypto-helper');
|
||||
const password_helper = require ('@sapphirecode/password-helper');
|
||||
|
||||
|
||||
/**
|
||||
* start the server
|
||||
*/
|
||||
async function start_server () {
|
||||
const app = express ();
|
||||
|
||||
const id = 69;
|
||||
const name = 'testuser';
|
||||
const salt = crypto.create_salt ();
|
||||
const password = await password_helper.hash (
|
||||
crypto.hash_sha512 ('foo', salt)
|
||||
);
|
||||
const user = { id, 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:${req.connection.user_id}`);
|
||||
});
|
||||
|
||||
return new Promise ((res) => {
|
||||
const listener = app.listen (0, () => {
|
||||
res (listener.address ().port);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { start_server };
|
39
package.json
39
package.json
@ -1,44 +1,33 @@
|
||||
{
|
||||
"name": "@sapphirecode/auth-server-helper",
|
||||
"version": "1.1.4",
|
||||
"version": "2.0.0",
|
||||
"main": "index.js",
|
||||
"author": {
|
||||
"name": "Timo Hocker",
|
||||
"email": "timo@scode.ovh"
|
||||
},
|
||||
"bugs": "https://redmine.scode.ovh/projects/auth-server-helper",
|
||||
"license": "MIT",
|
||||
"description": "authentication middleware for express",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"directory": "https://git.scode.ovh:timo/auth-server-helper.git"
|
||||
},
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@sapphirecode/auth-client-helper": "^1.0.45",
|
||||
"@sapphirecode/eslint-config": "^2.1.4",
|
||||
"@stryker-mutator/core": "^4.0.0",
|
||||
"@stryker-mutator/jasmine-runner": "^4.0.0",
|
||||
"@types/jasmine": "^3.5.14",
|
||||
"eslint": "^7.0.0",
|
||||
"express": "^4.17.1",
|
||||
"jasmine": "^3.6.1",
|
||||
"node-fetch": "^2.6.0",
|
||||
"nyc": "^15.0.1"
|
||||
"@sapphirecode/eslint-config-ts": "^1.1.27",
|
||||
"@types/jasmine": "^3.6.2",
|
||||
"jasmine": "^3.6.3",
|
||||
"jasmine-ts": "^0.3.0",
|
||||
"nyc": "^15.1.0",
|
||||
"ts-node": "^8.0.0",
|
||||
"typescript": "^4.1.2"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "eslint . --ext .js,.jsx,.ts,.tsx,.vue,.mjs",
|
||||
"test": "nyc jasmine --config=\"jasmine.json\"",
|
||||
"test": "nyc jasmine-ts --config=\"jasmine.json\"",
|
||||
"mutate": "stryker run",
|
||||
"compile": "tsc --allowJs --declaration --emitDeclarationOnly index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sapphirecode/consts": "^1.1.18",
|
||||
"@sapphirecode/crypto-helper": "^1.1.44",
|
||||
"@sapphirecode/password-helper": "^1.0.35"
|
||||
"compile": "tsc"
|
||||
},
|
||||
"files": [
|
||||
"LICENSE",
|
||||
"index.js"
|
||||
"*.js",
|
||||
"*.ts",
|
||||
"*.d.ts"
|
||||
],
|
||||
"keywords": [
|
||||
"authentication",
|
||||
|
@ -1,23 +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>, May 2020
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* @type {import('@stryker-mutator/api/core').StrykerOptions}
|
||||
*/
|
||||
module.exports = {
|
||||
packageManager: 'yarn',
|
||||
reporters: [
|
||||
'clear-text',
|
||||
'progress'
|
||||
],
|
||||
testRunner: 'jasmine',
|
||||
jasmineConfigFile: 'jasmine.json',
|
||||
coverageAnalysis: 'perTest',
|
||||
mutate: [ 'index.js' ]
|
||||
};
|
@ -1,123 +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>, May 2020
|
||||
*/
|
||||
|
||||
// @ts-nocheck
|
||||
/* eslint-disable no-undef */
|
||||
'use strict';
|
||||
|
||||
const mock_server = require ('../../mock_server');
|
||||
const client = require ('@sapphirecode/auth-client-helper');
|
||||
const consts = require ('@sapphirecode/consts');
|
||||
const fetch = require ('node-fetch');
|
||||
|
||||
let port = 0;
|
||||
|
||||
// eslint-disable-next-line max-lines-per-function
|
||||
describe ('server-helper', () => {
|
||||
beforeAll (async () => {
|
||||
port = await mock_server.start_server ();
|
||||
});
|
||||
|
||||
it ('should login', async () => {
|
||||
const session = await client.login (
|
||||
'testuser',
|
||||
'foo',
|
||||
`http://localhost:${port}`
|
||||
);
|
||||
expect (typeof session)
|
||||
.toEqual ('string');
|
||||
|
||||
const resp = await fetch (
|
||||
`http://localhost:${port}`,
|
||||
{ headers: { session } }
|
||||
);
|
||||
|
||||
expect (resp.status)
|
||||
.toEqual (consts.http.status_ok);
|
||||
expect (await resp.text ())
|
||||
.toEqual ('foo:69');
|
||||
});
|
||||
|
||||
it ('should allow access to excluded paths', async () => {
|
||||
const resp = await fetch (`http://localhost:${port}/noauthreg`);
|
||||
|
||||
expect (resp.status)
|
||||
.toEqual (consts.http.status_ok);
|
||||
expect (await resp.text ())
|
||||
.toEqual ('foo:undefined');
|
||||
});
|
||||
|
||||
it (
|
||||
'should allow access to excluded paths with correct method',
|
||||
async () => {
|
||||
const resp = await fetch (
|
||||
`http://localhost:${port}/noauthobj`,
|
||||
{ method: 'POST' }
|
||||
);
|
||||
|
||||
expect (resp.status)
|
||||
.toEqual (consts.http.status_ok);
|
||||
expect (await resp.text ())
|
||||
.toEqual ('foo:undefined');
|
||||
}
|
||||
);
|
||||
|
||||
it ('should reject access to excluded paths with wrong method', async () => {
|
||||
const resp = await fetch (
|
||||
`http://localhost:${port}/noauthobj`
|
||||
);
|
||||
|
||||
expect (resp.status)
|
||||
.toEqual (consts.http.status_unauthorized);
|
||||
});
|
||||
|
||||
it ('should reject invalid user', async () => {
|
||||
await expectAsync (client.login (
|
||||
'foo',
|
||||
'foo',
|
||||
`http://localhost:${port}`
|
||||
))
|
||||
.toBeRejectedWithError ('user or password invalid');
|
||||
});
|
||||
|
||||
it ('should reject and recover', async () => {
|
||||
await expectAsync (client.login (
|
||||
'testuser',
|
||||
'bar',
|
||||
`http://localhost:${port}`
|
||||
))
|
||||
.toBeRejectedWithError ('user or password invalid');
|
||||
|
||||
const session = await client.login (
|
||||
'testuser',
|
||||
'foo',
|
||||
`http://localhost:${port}`
|
||||
);
|
||||
expect (typeof session)
|
||||
.toEqual ('string');
|
||||
|
||||
const resp = await fetch (
|
||||
`http://localhost:${port}`,
|
||||
{ headers: { session } }
|
||||
);
|
||||
|
||||
expect (resp.status)
|
||||
.toEqual (consts.http.status_ok);
|
||||
expect (await resp.text ())
|
||||
.toEqual ('foo:69');
|
||||
});
|
||||
|
||||
it ('should reject invalid password', async () => {
|
||||
await expectAsync (client.login (
|
||||
'testuser',
|
||||
'bar',
|
||||
`http://localhost:${port}`
|
||||
))
|
||||
.toBeRejectedWithError ('user or password invalid');
|
||||
});
|
||||
});
|
||||
|
12
tsconfig.json
Normal file
12
tsconfig.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"module": "commonjs",
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./lib",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"declaration": true
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user