permissions, connection data reader
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Timo Hocker 2022-09-12 12:29:43 +02:00
parent 910099285b
commit ba9608829d
No known key found for this signature in database
GPG Key ID: 3B86485AC71C835C
8 changed files with 164 additions and 24 deletions

View File

@ -1,5 +1,10 @@
# Changelog
## 4.1.0
- Permission Management
- Gateway function to read connection info
## 4.0.0
- Blacklist entries can now be synchronized through redis

View File

@ -1,6 +1,6 @@
# auth-server-helper
version: 4.0.x
version: 4.1.x
customizable and simple authentication
@ -122,7 +122,8 @@ 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.
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, ...});
@ -136,12 +137,13 @@ 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.
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
By default all cookies will be sent with 'Secure; HttpOnly; SameSite=Strict'
Attributes
In the appropriate settings object, you can set the following options:
@ -158,7 +160,8 @@ In the appropriate settings object, you can set the following options:
}
```
For Documentation on the different Cookie Attributes see <https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#creating_cookies>
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
@ -176,14 +179,14 @@ const {GatewayClass} = require('@sapphirecode/auth-server-helper');
const gateway = new GatewayClass({ /* options */ });
// create a new express route
app.get('logout',(req,res)=>{
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
@ -198,7 +201,8 @@ const export = keystore.export_verification_data();
keystore.import_verification_data(export);
```
These keys can also be live synchronized with redis to allow sessions to be shared between servers
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');
@ -236,6 +240,51 @@ await blacklist.clear();
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>

View File

@ -20,6 +20,7 @@ interface AccessSettings {
redirect_to?: string
data?: unknown,
leave_open?: boolean
permissions?: string[]
}
interface AccessResult {
@ -95,7 +96,8 @@ class AuthRequest {
refresh_token_expires_in,
redirect_to,
data,
leave_open
leave_open,
permissions
}: AccessSettings): Promise<AccessResult> {
const log = logger.extend ('allow_access');
log ('allowed access');
@ -104,7 +106,7 @@ class AuthRequest {
const at = await auth.sign (
'access_token',
access_token_expires_in,
{ data }
{ data, permissions }
);
const result: AccessResult = { access_token_id: at.id };

View File

@ -25,6 +25,7 @@ interface VerificationResult {
type: TokenType;
id: string;
next_module?: string;
permissions?: string[];
data?: unknown;
error?: string;
}
@ -37,7 +38,8 @@ interface SignatureResult {
interface SignatureOptions
{
data?: unknown
next_module?: string
next_module?: string,
permissions?: string[]
}
class Authority {
@ -45,10 +47,11 @@ class Authority {
const log = logger.extend ('verify');
log ('verifying token');
const result: VerificationResult = {
authorized: false,
valid: false,
type: 'none',
id: ''
authorized: false,
valid: false,
type: 'none',
permissions: [],
id: ''
};
const data = await verify_signature_get_info (
key,
@ -83,6 +86,7 @@ class Authority {
result.valid = true;
result.authorized = result.type === 'access_token';
result.next_module = data.next_module;
result.permissions = data.permissions;
result.data = data.obj;
log (
@ -90,6 +94,7 @@ class Authority {
result.type,
result.next_module
);
log ('permissions %o', result.permissions);
return result;
}
@ -111,7 +116,8 @@ class Authority {
type,
valid_for,
valid_until,
next_module: options?.next_module
next_module: options?.next_module,
permissions: options?.permissions
};
const signature = sign_object (options?.data, key, attributes);
log ('created token %s', attributes.id);

View File

@ -32,6 +32,13 @@ interface GatewayOptions {
cookie?: CookieSettings;
refresh_cookie?: CookieSettings;
refresh_settings?: RefreshSettings;
require_permissions?: string[];
}
interface ConnectionInfo {
token_id: string
token_data: unknown
permissions: string[]
}
class GatewayClass {
@ -97,7 +104,11 @@ class GatewayClass {
log ('setting connection info');
const con = req.connection as unknown as Record<string, unknown>;
con.auth = { token_id: ver.id, token_data: ver.data };
con.auth = {
token_id: ver.id,
token_data: ver.data,
permissions: ver.permissions
};
log ('token valid: %s', ver.authorized);
return ver.authorized;
@ -144,8 +155,9 @@ class GatewayClass {
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
token_id: refresh_result.access_token_id,
token_data: ver.data,
permissions: ver.permissions
};
log ('tokens refreshed');
@ -175,6 +187,22 @@ class GatewayClass {
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,
@ -183,7 +211,13 @@ class GatewayClass {
const log = logger.extend ('process_request');
log ('processing incoming http request');
if (await this.authenticate (req, res)) {
log ('authentification successful, calling next handler');
log ('authentification successful');
log ('checking permissions');
if (!this.check_permissions (req))
return this.redirect (res);
log ('authorization successful. calling next handler');
return next ();
}
@ -216,6 +250,16 @@ class GatewayClass {
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 {

View File

@ -1,6 +1,6 @@
{
"name": "@sapphirecode/auth-server-helper",
"version": "4.0.2",
"version": "4.1.0",
"main": "dist/lib/index.js",
"author": {
"name": "Timo Hocker",

View File

@ -36,8 +36,13 @@ describe ('gateway', () => {
g.logout (req);
}
res.writeHead (200);
const con = req.connection as unknown as Record<string, unknown>;
res.end (JSON.stringify (con.auth));
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);
});
@ -112,6 +117,8 @@ describe ('gateway', () => {
.toEqual (token.id);
expect (body.token_data)
.toEqual ('foobar');
expect (body.permissions)
.toEqual ([]);
});
it ('should reject an outdated access token', async () => {
@ -198,4 +205,22 @@ describe ('gateway', () => {
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
});
});
});

View File

@ -1,6 +1,15 @@
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;