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 # Changelog
## 4.1.0
- Permission Management
- Gateway function to read connection info
## 4.0.0 ## 4.0.0
- Blacklist entries can now be synchronized through redis - Blacklist entries can now be synchronized through redis

View File

@ -1,6 +1,6 @@
# auth-server-helper # auth-server-helper
version: 4.0.x version: 4.1.x
customizable and simple authentication 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 #### 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 ```js
allow_access({leave_open: true, ...}); allow_access({leave_open: true, ...});
@ -136,12 +137,13 @@ invalid('error description', true);
deny(true); deny(true);
``` ```
if this flag is set, no data will be written to the response body and no data will be sent. if this flag is set, no data will be written to the response body and no data
Status code and Headers will still be set. will be sent. Status code and Headers will still be set.
### Defining Custom Cookie Settings ### 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: 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 ### 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 */ }); const gateway = new GatewayClass({ /* options */ });
// create a new express route // create a new express route
app.get('logout',(req,res)=>{ app.get('logout', (req, res) => {
// call the gateway's logout function // call the gateway's logout function
gateway.logout(req); gateway.logout(req);
// respond ok // respond ok
res.status(200); res.status(200);
res.end(); res.end();
}) });
``` ```
### Exporting and importing public keys to validate tokens across server instances ### 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); 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 ```js
const {keystore} = require('@sapphirecode/auth-server-helper'); const {keystore} = require('@sapphirecode/auth-server-helper');
@ -236,6 +240,51 @@ await blacklist.clear();
await blacklist.clear(Date.now() - 10000); 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 ## License
MIT © Timo Hocker <timo@scode.ovh> MIT © Timo Hocker <timo@scode.ovh>

View File

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

View File

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

View File

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

View File

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

View File

@ -36,8 +36,13 @@ describe ('gateway', () => {
g.logout (req); g.logout (req);
} }
res.writeHead (200); res.writeHead (200);
const con = req.connection as unknown as Record<string, unknown>; const data = {
res.end (JSON.stringify (con.auth)); ...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); g.process_request (req, res, passed_handler);
}); });
@ -112,6 +117,8 @@ describe ('gateway', () => {
.toEqual (token.id); .toEqual (token.id);
expect (body.token_data) expect (body.token_data)
.toEqual ('foobar'); .toEqual ('foobar');
expect (body.permissions)
.toEqual ([]);
}); });
it ('should reject an outdated access token', async () => { it ('should reject an outdated access token', async () => {
@ -198,4 +205,22 @@ describe ('gateway', () => {
expect (blacklisted) expect (blacklisted)
.toContain (refresh.id); .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 { generate_token_id, parse_token_id } from '../../lib/token_id';
import { clock_finalize, clock_setup } from '../Helper';
describe ('token_id', () => { describe ('token_id', () => {
beforeAll (() => {
clock_setup ();
});
afterAll (() => {
clock_finalize ();
});
it ('should always generate valid tokens', () => { it ('should always generate valid tokens', () => {
for (let i = 0; i < 1000; i++) { for (let i = 0; i < 1000; i++) {
const date = new Date; const date = new Date;