next();
};
- const accountFromToken = (token, req, next) => {
+ const accountFromToken = (token, allowedScopes, req, next) => {
pgPool.connect((err, client, done) => {
if (err) {
next(err);
return;
}
- client.query('SELECT oauth_access_tokens.resource_owner_id, users.account_id, users.chosen_languages FROM oauth_access_tokens INNER JOIN users ON oauth_access_tokens.resource_owner_id = users.id WHERE oauth_access_tokens.token = $1 AND oauth_access_tokens.revoked_at IS NULL LIMIT 1', [token], (err, result) => {
+ client.query('SELECT oauth_access_tokens.resource_owner_id, users.account_id, users.chosen_languages, oauth_access_tokens.scopes FROM oauth_access_tokens INNER JOIN users ON oauth_access_tokens.resource_owner_id = users.id WHERE oauth_access_tokens.token = $1 AND oauth_access_tokens.revoked_at IS NULL LIMIT 1', [token], (err, result) => {
done();
if (err) {
return;
}
+ const scopes = result.rows[0].scopes.split(' ');
+
+ if (allowedScopes.size > 0 && !scopes.some(scope => allowedScopes.includes(scope))) {
+ err = new Error('Access token does not cover required scopes');
+ err.statusCode = 401;
+
+ next(err);
+ return;
+ }
+
req.accountId = result.rows[0].account_id;
req.chosenLanguages = result.rows[0].chosen_languages;
+ req.allowNotifications = scopes.some(scope => ['read', 'read:notifications'].includes(scope));
next();
});
});
};
- const accountFromRequest = (req, next, required = true) => {
+ const accountFromRequest = (req, next, required = true, allowedScopes = ['read']) => {
const authorization = req.headers.authorization;
const location = url.parse(req.url, true);
- const accessToken = location.query.access_token;
+ const accessToken = location.query.access_token || req.headers['sec-websocket-protocol'];
if (!authorization && !accessToken) {
if (required) {
const token = authorization ? authorization.replace(/^Bearer /, '') : accessToken;
- accountFromToken(token, req, next);
+ accountFromToken(token, allowedScopes, req, next);
};
const PUBLIC_STREAMS = [
const wsVerifyClient = (info, cb) => {
const location = url.parse(info.req.url, true);
const authRequired = !PUBLIC_STREAMS.some(stream => stream === location.query.stream);
+ const allowedScopes = [];
+
+ if (authRequired) {
+ allowedScopes.push('read');
+ if (location.query.stream === 'user:notification') {
+ allowedScopes.push('read:notifications');
+ } else {
+ allowedScopes.push('read:statuses');
+ }
+ }
accountFromRequest(info.req, err => {
if (!err) {
log.error(info.req.requestId, err.toString());
cb(false, 401, 'Unauthorized');
}
- }, authRequired);
+ }, authRequired, allowedScopes);
};
const PUBLIC_ENDPOINTS = [
}
const authRequired = !PUBLIC_ENDPOINTS.some(endpoint => endpoint === req.path);
- accountFromRequest(req, next, authRequired);
+ const allowedScopes = [];
+
+ if (authRequired) {
+ allowedScopes.push('read');
+ if (req.path === '/api/v1/streaming/user/notification') {
+ allowedScopes.push('read:notifications');
+ } else {
+ allowedScopes.push('read:statuses');
+ }
+ }
+
+ accountFromRequest(req, next, authRequired, allowedScopes);
};
const errorMiddleware = (err, req, res, {}) => {
return;
}
+ if (event === 'notification' && !req.allowNotifications) {
+ return;
+ }
+
// Only messages that may require filtering are statuses, since notifications
// are already personalized and deletes do not matter
if (!needsFiltering || event !== 'update') {