diff --git a/CHANGELOG.md b/CHANGELOG.md index 62d990b..19d85ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index af1c289..844de49 100644 --- a/README.md +++ b/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 +For Documentation on the different Cookie Attributes see + ### 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 diff --git a/lib/AuthHandler.ts b/lib/AuthHandler.ts index 3e44aee..b2b7bd7 100644 --- a/lib/AuthHandler.ts +++ b/lib/AuthHandler.ts @@ -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 { 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 }; diff --git a/lib/Authority.ts b/lib/Authority.ts index 8f503a9..227dd24 100644 --- a/lib/Authority.ts +++ b/lib/Authority.ts @@ -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); diff --git a/lib/Gateway.ts b/lib/Gateway.ts index 6171a0b..9600e78 100644 --- a/lib/Gateway.ts +++ b/lib/Gateway.ts @@ -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; - 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; 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; + const auth = conn.auth as Record; + 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 { diff --git a/package.json b/package.json index 25d2c80..82ff056 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/test/spec/Gateway.ts b/test/spec/Gateway.ts index 5e1e851..10e8b0e 100644 --- a/test/spec/Gateway.ts +++ b/test/spec/Gateway.ts @@ -36,8 +36,13 @@ describe ('gateway', () => { g.logout (req); } res.writeHead (200); - const con = req.connection as unknown as Record; - 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 + }); + }); }); diff --git a/test/spec/token_id.ts b/test/spec/token_id.ts index ba207ca..8b64fcf 100644 --- a/test/spec/token_id.ts +++ b/test/spec/token_id.ts @@ -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;