Skip to content

Commit

Permalink
Add support for generating a service token
Browse files Browse the repository at this point in the history
  • Loading branch information
humphd committed Apr 5, 2021
1 parent fb38fae commit fc03dd6
Show file tree
Hide file tree
Showing 5 changed files with 118 additions and 2 deletions.
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,8 @@ router.get('/private-route', isAuthenticated(), (req, res) => {...});
Here are some examples:
```js
const { isAuthenticated, isAuthorized } = require("@senecacdot/satellite");
// Authorize based on `roles`
router.get('/admin', isAuthenticated(), isAuthorized({ roles: ["admin"] }), (req, res) => {...});
Expand Down Expand Up @@ -213,6 +215,36 @@ const { hash } = require('@senecacdot/satellite');
const id = hash('http://someurl.com');
```
### Create Service Token
Services authorize requests using the `isAuthenticated()` and `isAuthorized()` middleware discussed above.
For the most part, this is meant to be used for the case of user-to-service requests: an authenticated
user passes a JWT token (acquired via the `auth` service), and uses it to request authorization to some
protected route.
However, in cases where you need to do a service-to-service request, you can use the `createServiceToken()`
function in order to get a short-lived access token that will include the `"service"` role:
```js
const { createServiceToken } = require('@senecacdot/satellite');
...
const res = await fetch(`some/protected/route`, {
headers: {
Authorization: `bearer ${createServiceToken()}`,
},
});
```
The receiving service can then opt-into allowing this service to be authorized by using
the `isAuthenticated()` and `isAuthorized()` middleware like so:
```js
const { isAuthenticated, isAuthorized } = require("@senecacdot/satellite");
// Allow requests with a token bearing the 'service' role to proceed
router.get('/admin-or-service', isAuthenticated(), isAuthorized({ roles: ["service"] }), (req, res) => {...});
```
### Create Error
The `createError()` function creates a unique HTTP Error Object which is based on [http-errors](https://www.npmjs.com/package/http-errors).
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"express-pino-logger": "^6.0.0",
"helmet": "4.4.1",
"http-errors": "^1.8.0",
"jsonwebtoken": "^8.5.1",
"pino": "^6.11.2",
"pino-colada": "^2.1.0"
},
Expand All @@ -42,7 +43,6 @@
"get-port": "^5.1.1",
"husky": "^5.1.3",
"jest": "^26.6.3",
"jsonwebtoken": "^8.5.1",
"node-fetch": "^2.6.1",
"prettier": "2.2.1",
"pretty-quick": "3.1.0"
Expand Down
1 change: 1 addition & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ module.exports.Satellite = require('./satellite');
module.exports.logger = require('./logger');
module.exports.hash = require('./hash');
module.exports.createError = require('http-errors');
module.exports.createServiceToken = require('./service-token');
module.exports.Router = (options) => createRouter(options);
module.exports.isAuthenticated = isAuthenticated;
module.exports.isAuthorized = isAuthorized;
22 changes: 22 additions & 0 deletions src/service-token.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
const jwt = require('jsonwebtoken');

const { JWT_ISSUER, JWT_AUDIENCE, SECRET } = process.env;

/**
* Create a short-lived service-to-service JWT, useful for authorizing
* one Telescope microservice with another. The receiving service has
* to opt into this by allowing the 'service' role.
* @returns JWT service token
*/
function createServiceToken() {
const payload = {
iss: JWT_ISSUER,
aud: JWT_AUDIENCE,
sub: 'telescope-service',
roles: ['service'],
};

return jwt.sign(payload, SECRET, { expiresIn: '5m' });
}

module.exports = createServiceToken;
63 changes: 62 additions & 1 deletion test.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const {
logger,
hash,
createError,
createServiceToken,
} = require('./src');
const { JWT_EXPIRES_IN, JWT_ISSUER, JWT_AUDIENCE, SECRET } = process.env;

Expand Down Expand Up @@ -427,7 +428,6 @@ describe('Satellite()', () => {
isAuthorized({
authorizeUser: (user) => {
expect(user).toEqual(decoded);
console.log({ user, decoded });
return user.sub === '[email protected]';
},
}),
Expand Down Expand Up @@ -457,6 +457,53 @@ describe('Satellite()', () => {
});
});

test('isAuthenticated() + isAuthorized() for service token and role should work on a specific route', (done) => {
const service = createSatelliteInstance({
name: 'test',
});
const token = createServiceToken();

const router = service.router;
router.get('/public', (req, res) => res.json({ hello: 'public' }));
router.get(
'/protected',
isAuthenticated(),
isAuthorized({ roles: ['service'] }),
(req, res) => {
// Make sure an admin user payload was added to req
expect(req.user.sub).toEqual('telescope-service');
expect(Array.isArray(req.user.roles)).toBe(true);
expect(req.user.roles).toContain('service');
res.json({ hello: 'protected' });
}
);

service.start(port, async () => {
// Public should need no bearer token
let res = await fetch(`${url}/public`);
expect(res.ok).toBe(true);
let body = await res.json();
expect(body).toEqual({ hello: 'public' });

// Protected should fail without authorization header
res = await fetch(`${url}/protected`);
expect(res.ok).toBe(false);
expect(res.status).toEqual(401);

// Protected should work with authorization header
res = await fetch(`${url}/protected`, {
headers: {
Authorization: `bearer ${token}`,
},
});
expect(res.ok).toBe(true);
body = await res.json();
expect(body).toEqual({ hello: 'protected' });

service.stop(done);
});
});

test('isAuthenticated() + isAuthorized() for admin role should work on a specific route', (done) => {
const service = createSatelliteInstance({
name: 'test',
Expand Down Expand Up @@ -821,3 +868,17 @@ describe('Create Error tests for Satellite', () => {
expect(testError.message).toBe('Satellite Test for Errors');
});
});

describe('createServiceToken()', () => {
test('should create a service token', () => {
const token = createServiceToken();
const decoded = jwt.verify(token, SECRET);

expect(decoded.sub).toEqual('telescope-service');
expect(Array.isArray(decoded.roles)).toBe(true);
expect(decoded.roles).toContain('service');

const currentDateSeconds = Date.now() / 1000;
expect(decoded.exp).toBeGreaterThan(currentDateSeconds);
});
});

0 comments on commit fc03dd6

Please sign in to comment.