redesign
Some checks failed
continuous-integration/drone/push Build is failing

This commit is contained in:
Timo Hocker 2020-12-03 09:54:27 +01:00
parent fe037d43d3
commit 210696dda0
14 changed files with 2245 additions and 3895 deletions

View File

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

View File

@ -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
},
extends: [ '@sapphirecode' ],
extends: [
'@sapphirecode'
],
globals: {
Atomics: 'readonly',
SharedArrayBuffer: 'readonly'
},
parserOptions: { ecmaVersion: 2018 }
};
parserOptions: {
ecmaVersion: 2018
}
}

3
.gitignore vendored
View File

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

View File

@ -4,5 +4,5 @@
"author": "Timo Hocker",
"company": "Sapphirecode",
"email": "timo@scode.ovh",
"software": "auth-server-helper"
"software": "Auth-Server-Helper"
}

View File

@ -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

View File

@ -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
View File

@ -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
View 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
}
}

View File

@ -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 };

View File

@ -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",

View File

@ -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' ]
};

View File

@ -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
View File

@ -0,0 +1,12 @@
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"outDir": "./dist",
"rootDir": "./lib",
"strict": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"declaration": true
}
}

3078
yarn.lock

File diff suppressed because it is too large Load Diff