Skip to content
This repository has been archived by the owner on Nov 13, 2020. It is now read-only.

Upgrade s3o-middleware-utils to 2.0.0 #53

Merged
merged 2 commits into from
Mar 26, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 35 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,42 +1,52 @@
# S3O-middleware

Middleware to handle authenticating with [S3O](http://s3o.ft.com/docs)

## Parsing cookies

This middleware can parse standard cookies via the [cookie](http://npmjs.com/package/cookie) package. If wanting to use signed cookies or json cookies, please use the [cookie-parser](https://www.npmjs.com/package/cookie-parser) middleware before using the S3O middleware.

# Finding the username of the logged in user
## Finding the username of the logged in user

The username can be found in the request cookie, under `req.cookies.s3o_username`.

# Setting the ttl of the cookie for an authenticated request
## Setting the ttl of the cookie for an authenticated request

Defaults to fifteen minutes. Use Express' `app.set` function before sending users to authenticate:
`app.set('s3o-cookie-ttl', 86400000); // one day (in ms)`

## Usage example for Express

If many routes require auth:

```js
var express = require('express');
var app = express();
const express = require('express');
const app = express();

// Add routes here which don't require auth
var authS3O = require('@financial-times/s3o-middleware');
const authS3O = require('@financial-times/s3o-middleware');
app.use(authS3O);
// Add routes here which require auth
```

If only paths within a given directory require auth:

```js
var express = require('express');
var app = express();
var router = express.Router();
var authS3O = require('@financial-times/s3o-middleware');
const express = require('express');
const app = express();
const router = express.Router();
const authS3O = require('@financial-times/s3o-middleware');
router.use(authS3O);
app.use('/admin', router);
```

If specific paths require auth:

```js
var express = require('express');
var app = express();
var router = express.Router();
var authS3O = require('@financial-times/s3o-middleware');
const express = require('express');
const app = express();
const router = express.Router();
const authS3O = require('@financial-times/s3o-middleware');

app.get('/', authS3O, router);
app.post('/', authS3O);
Expand All @@ -49,10 +59,10 @@ cookies are not present or are invalid, the `authS3ONoRedirect`
middleware will respond with a simple `403: Forbidden` response:

```js
var express = require('express');
var app = express();
var router = express.Router();
var authS3ONoRedirect = require('@financial-times/s3o-middleware').authS3ONoRedirect;
const express = require('express');
const app = express();
const router = express.Router();
const { authS3ONoRedirect } = require('@financial-times/s3o-middleware');

app.get('/some-authenticated-api', authS3ONoRedirect, router);
```
Expand All @@ -62,19 +72,20 @@ redirect to the http version. You can override this by adding express middleware
to force the protocol of the redirect url.

```js
app.use('/', function (req, res, next) {
req.headers['x-forwarded-proto'] = 'https';
next();
app.use('/', function(req, res, next) {
req.headers['x-forwarded-proto'] = 'https';
next();
});
```

### Upgrade to s3o version 4

Set `x-s3o-version` header to 'v4' and optionally pass a system-code header `x-s3o-systemcode`

```js
app.use('/', function (req, res, next) {
req.headers['x-s3o-version'] = 'v4';
req.headers['x-s3o-systemcode'] = 'your-system-code';
next();
app.use('/', function(req, res, next) {
req.headers['x-s3o-version'] = 'v4';
req.headers['x-s3o-systemcode'] = 'your-system-code';
next();
});
```
43 changes: 27 additions & 16 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,17 @@
// — Currently it comes in DER format and needs to be converted to PEM format

const debug = require('debug')('middleware:auth:s3o');
const querystring = require('querystring');
const url = require('url');
const urlencoded = require('body-parser').urlencoded({extended: true});
const { authenticateToken, validate, s3oPublicKeyPromise } = require('@financial-times/s3o-middleware-utils/authenticate');
const { normaliseRequestCookies, setCookies, clearCookies } = require('@financial-times/s3o-middleware-utils/cookies');
const { authenticate, publickey, cookies } = require('@financial-times/s3o-middleware-utils');
const { getUsername, getToken, normaliseRequestCookies, setCookies, clearCookies } = require('./lib/cookies');

let authS3O = function (req, res, next) {
const { USERNAME: S3O_USERNAME_COOKIE } = cookies;
const publicKeyPoller = publickey.poller(debug);
const { authenticateToken, validate } = authenticate(publickey.poller(debug));

const authS3O = function (req, res, next) {
debug('S3O: Start.');

normaliseRequestCookies(req);
Expand All @@ -20,7 +25,7 @@ let authS3O = function (req, res, next) {
// These parameters come from https://s3o.ft.com. It redirects back after it does the google authentication.
if (req.method === 'POST' && req.query.username) {
urlencoded(req, res, function () {
debug('S3O: Found parameter token for s3o_username: ' + req.query.username);
debug(`S3O: Found parameter token for ${S3O_USERNAME_COOKIE}: ${req.query.username}`);
let isAuthenticated;
try {
isAuthenticated = authenticateToken(req.query.username, req.hostname, req.body.token);
Expand Down Expand Up @@ -54,12 +59,12 @@ let authS3O = function (req, res, next) {
});

// Check for s3o username/token cookies
} else if (req.cookies.s3o_username && req.cookies.s3o_token) {
debug('S3O: Found cookie token for s3o_username: ' + req.cookies.s3o_username);
} else if (getUsername(req) && getToken(req)) {
debug(`S3O: Found cookie token for ${S3O_USERNAME_COOKIE}: ${getUsername(req)}`);

let isAuthenticated;
try {
isAuthenticated = authenticateToken(req.cookies.s3o_username, req.hostname, req.cookies.s3o_token);
isAuthenticated = authenticateToken(getUsername(req), req.hostname, getToken(req));
} catch(e) {
res.status(500).send(e);
return;
Expand All @@ -76,16 +81,21 @@ let authS3O = function (req, res, next) {
const s3o_system_code = req.headers['x-s3o-systemcode'];
const protocol = (req.headers['x-forwarded-proto'] && req.headers['x-forwarded-proto'] === 'https') ? 'https' : req.protocol;
const originalLocation = protocol + '://' + req.hostname + req.originalUrl;
const s3o_url_v2 = 'https://s3o.ft.com/v2/authenticate?post=true&host=' + encodeURIComponent(req.hostname) + '&redirect=' + encodeURIComponent(originalLocation);
let s3o_url_v4 = 'https://s3ov4.in.ft.com/v2/authenticate?post=true&host=' + encodeURIComponent(req.hostname) + '&redirect=' + encodeURIComponent(originalLocation);
const parameters = querystring.stringify({
post: true,
host: req.hostname,
redirect: originalLocation,
});
const s3o_url_v2 = `https://s3o.ft.com/v2/authenticate?${parameters}`;
let s3o_url_v4 = `https://s3ov4.in.ft.com/v2/authenticate?${parameters}`;

if (s3o_system_code) {
s3o_url_v4 += '&systemcode='+ encodeURIComponent(s3o_system_code);
}

const s3o_url = req.headers['x-s3o-version'] === 'v4' ? s3o_url_v4 : s3o_url_v2;

debug('S3O: No token/s3o_username found. Redirecting to ' + s3o_url);
debug(`S3O: No token/${S3O_USERNAME_COOKIE} found. Redirecting to ${s3o_url}`);

// Don't cache any redirection responses.
res.header('Cache-Control', 'private, no-cache, no-store, must-revalidate');
Expand All @@ -98,20 +108,21 @@ let authS3O = function (req, res, next) {
// Alternative authentication middleware which does not redirect to S3O when
// cookies are missing or invalid. This can be used in front of API calls
// where a redirect will be undesirable
let authS3ONoRedirect = function (req, res, next) {
const authS3ONoRedirect = function (req, res, next) {
debug('S3O: Start.');

normaliseRequestCookies(req);

if (req.cookies.s3o_username && req.cookies.s3o_token && authenticateToken(req.cookies.s3o_username, req.hostname, req.cookies.s3o_token)) {
const username = getUsername(req);
const token = getToken(req);

if (username && token && authenticateToken(username, req.hostname, token)) {
debug('S3O: Authentication succeeded');
return next();
};

debug('S3O: Authentication failed');
res.clearCookie('s3o_username');
res.clearCookie('s3o_token');
res.status(403);
clearCookies(res);
res.send('Forbidden');

return false;
Expand All @@ -121,4 +132,4 @@ let authS3ONoRedirect = function (req, res, next) {
module.exports = authS3O;
module.exports.authS3ONoRedirect = authS3ONoRedirect;
module.exports.validate = validate;
module.exports.ready = s3oPublicKeyPromise.then(function () { return true; });
module.exports.ready = publicKeyPoller({ promise: true }).then(() => true);
79 changes: 79 additions & 0 deletions lib/cookies.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/**
* Sundry utilities for cookies.
*
*/
const { cookies } = require('@financial-times/s3o-middleware-utils');
const { USERNAME, TOKEN, DEFAULT_EXPIRY } = cookies;
const { parse: cookieParser } = require('cookie');

/**
* Normalise cookies coming from Express
*
* @param {object} req Express Request object
* @return {void}
*/
const normaliseRequestCookies = function (req) {
if (req.cookies === undefined || req.cookies === null) {
const cookies = req.headers.cookie;
if (cookies) {
req.cookies = cookieParser(cookies);
} else {
req.cookies = Object.create(null);
}
}
};

/**
* Gets s3o username from request cookies
*
* @param {object} req Express request object
* @return {string|undefined} The S3O username if it exists
*/
const getUsername = req => req.cookies[USERNAME];

/**
* Gets s3o username from request cookies
*
* @param {object} req Express request object
* @return {string|undefined} The S3O token if it exists
*/
const getToken = req => req.cookies[TOKEN];

/**
* Sets Express request cookies
*
* @param {object} res Express result object
* @param {string} username Google username
* @param {string} token S3O token
* @return {void}
*/
const setCookies = function (res, username, token) {
// Add username to res.locals, so apps can utilise it.
res.locals.s3o_username = username;
const cookieOptions = {
maxAge: res.app.get('s3o-cookie-ttl') || DEFAULT_EXPIRY,
httpOnly: true,
};
res.cookie(USERNAME, username, cookieOptions);
res.cookie(TOKEN, token, cookieOptions);
};

/**
* Clears the cookies and sends 403 status
*
* @param {object} res Express result object
* @return {void}
*/
const clearCookies = function (res) {
res.clearCookie(USERNAME);
res.clearCookie(TOKEN);
res.status(403);
};

module.exports = {
setCookies,
getUsername,
getToken,
normaliseRequestCookies,
clearCookies,
};
Loading