permissions, connection data reader
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
parent
910099285b
commit
ba9608829d
@ -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
|
||||
|
65
README.md
65
README.md
@ -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
|
||||
|
||||
@ -183,7 +186,7 @@ app.get('logout',(req,res)=>{
|
||||
// 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>
|
||||
|
@ -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 };
|
||||
|
||||
|
@ -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 {
|
||||
@ -48,6 +50,7 @@ class Authority {
|
||||
authorized: false,
|
||||
valid: false,
|
||||
type: 'none',
|
||||
permissions: [],
|
||||
id: ''
|
||||
};
|
||||
const data = await verify_signature_get_info (
|
||||
@ -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);
|
||||
|
@ -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;
|
||||
@ -145,7 +156,8 @@ class GatewayClass {
|
||||
const con = req.connection as unknown as Record<string, unknown>;
|
||||
con.auth = {
|
||||
token_id: refresh_result.access_token_id,
|
||||
token_data: ver.data
|
||||
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 {
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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;
|
||||
|
Loading…
x
Reference in New Issue
Block a user