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
|
# 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
|
||||||
|
67
README.md
67
README.md
@ -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>
|
||||||
|
@ -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 };
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
@ -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 {
|
||||||
|
@ -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",
|
||||||
|
@ -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
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -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;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user