Use a token to protect POST API queries

This commit is contained in:
Lucas Verney 2018-07-18 15:30:30 +02:00
parent 20a162e915
commit c5183a570f
5 changed files with 58 additions and 4 deletions

View File

@ -49,6 +49,8 @@ adapt the behavior to your needs.
to `/`). The value should end with a trailing slash. to `/`). The value should end with a trailing slash.
* `THUNDERFOREST_API_KEY=` to pass an API key server to use for * `THUNDERFOREST_API_KEY=` to pass an API key server to use for
[Thunderforest](http://thunderforest.com/) tiles (OpenCycleMap, etc). [Thunderforest](http://thunderforest.com/) tiles (OpenCycleMap, etc).
* `API_TOKEN=` to pass a token required to access the server side API (check
below in the server part environment variables for more details).
You should also have a look at the build variables under the `config/` You should also have a look at the build variables under the `config/`
subdirectory. subdirectory.
@ -79,6 +81,7 @@ adapt its behavior:
* `DATABASE=` to specify a [database URL](http://docs.peewee-orm.com/en/latest/peewee/playhouse.html#db-url) to connect to (defaults to * `DATABASE=` to specify a [database URL](http://docs.peewee-orm.com/en/latest/peewee/playhouse.html#db-url) to connect to (defaults to
`sqlite:///reports.db` which means a SQLite database named `reports.db` in `sqlite:///reports.db` which means a SQLite database named `reports.db` in
the current working directory). the current working directory).
* `API_TOKEN=` to specify a token required to `POST` data to the API.
#### Serving in production #### Serving in production

View File

@ -2,5 +2,6 @@
module.exports = { module.exports = {
NODE_ENV: '"production"', NODE_ENV: '"production"',
API_BASE_URL: JSON.stringify(process.env.API_BASE_URL), API_BASE_URL: JSON.stringify(process.env.API_BASE_URL),
API_TOKEN: JSON.stringify(process.env.API_TOKEN),
THUNDERFOREST_API_KEY: JSON.stringify(process.env.THUNDERFOREST_API_KEY), THUNDERFOREST_API_KEY: JSON.stringify(process.env.THUNDERFOREST_API_KEY),
} }

View File

@ -34,7 +34,8 @@ def enable_cors():
'PUT, GET, POST, DELETE, OPTIONS, PATCH' 'PUT, GET, POST, DELETE, OPTIONS, PATCH'
) )
bottle.response.headers[str('Access-Control-Allow-Headers')] = str( bottle.response.headers[str('Access-Control-Allow-Headers')] = str(
'Origin, Accept, Content-Type, X-Requested-With, X-CSRF-Token' 'Origin, Accept, Content-Type, X-Requested-With, X-CSRF-Token, '
'Authorization'
) )

View File

@ -5,6 +5,7 @@ Routes definitions
""" """
import arrow import arrow
import json import json
import os
import bottle import bottle
@ -12,6 +13,29 @@ from server.models import Report
from server import jsonapi from server import jsonapi
class AuthenticationError(Exception):
pass
def check_auth():
"""
Check authentication.
:return: Abort and return a HTTP 403 page if authentication is not ok.
"""
if not os.getenv('API_TOKEN'):
return
auth = bottle.request.headers.get('Authorization', None)
if not auth:
raise AuthenticationError()
parts = auth.split()
if parts[0].lower() != 'bearer' or parts[1] != os.getenv('API_TOKEN'):
raise AuthenticationError()
return
def get_reports(only_active=False): def get_reports(only_active=False):
""" """
Get reports for the reports getting routes. Get reports for the reports getting routes.
@ -151,6 +175,12 @@ def post_report():
if bottle.request.method == 'OPTIONS': if bottle.request.method == 'OPTIONS':
return {} return {}
# Check authentication
try:
check_auth()
except AuthenticationError:
return jsonapi.JsonApiError(403, "Invalid authentication.")
try: try:
payload = json.load(bottle.request.body) payload = json.load(bottle.request.body)
except ValueError as exc: except ValueError as exc:
@ -189,6 +219,12 @@ def upvote_report(id):
if bottle.request.method == 'OPTIONS': if bottle.request.method == 'OPTIONS':
return {} return {}
# Check authentication
try:
check_auth()
except AuthenticationError:
return jsonapi.JsonApiError(403, "Invalid authentication.")
r = Report.get(Report.id == id) r = Report.get(Report.id == id)
if not r: if not r:
return jsonapi.JsonApiError(404, "Invalid report id.") return jsonapi.JsonApiError(404, "Invalid report id.")
@ -213,6 +249,12 @@ def downvote_report(id):
if bottle.request.method == 'OPTIONS': if bottle.request.method == 'OPTIONS':
return {} return {}
# Check authentication
try:
check_auth()
except AuthenticationError:
return jsonapi.JsonApiError(403, "Invalid authentication.")
r = Report.get(Report.id == id) r = Report.get(Report.id == id)
if not r: if not r:
return jsonapi.JsonApiError(404, "Invalid report id.") return jsonapi.JsonApiError(404, "Invalid report id.")

View File

@ -3,6 +3,10 @@ require('isomorphic-fetch');
// With trailing slash // With trailing slash
export const BASE_URL = process.env.API_BASE_URL || '/'; export const BASE_URL = process.env.API_BASE_URL || '/';
const AUTHORIZATION_HEADERS = new Headers({});
if (process.env.API_TOKEN) {
AUTHORIZATION_HEADERS.set('Authorization', `Bearer ${process.env.API_TOKEN}`);
}
export function saveReport(type, lat, lng) { export function saveReport(type, lat, lng) {
return fetch(`${BASE_URL}api/v1/reports`, { return fetch(`${BASE_URL}api/v1/reports`, {
@ -12,6 +16,7 @@ export function saveReport(type, lat, lng) {
lat, lat,
lng, lng,
}), }),
headers: AUTHORIZATION_HEADERS,
}) })
.then(response => response.json()) .then(response => response.json())
.then(response => response.data) .then(response => response.data)
@ -34,6 +39,7 @@ export function getActiveReports() {
export function downvote(id) { export function downvote(id) {
return fetch(`${BASE_URL}api/v1/reports/${id}/downvote`, { return fetch(`${BASE_URL}api/v1/reports/${id}/downvote`, {
method: 'POST', method: 'POST',
headers: AUTHORIZATION_HEADERS,
}) })
.then(response => response.json()) .then(response => response.json())
.then(response => response.data) .then(response => response.data)
@ -46,6 +52,7 @@ export function downvote(id) {
export function upvote(id) { export function upvote(id) {
return fetch(`${BASE_URL}api/v1/reports/${id}/upvote`, { return fetch(`${BASE_URL}api/v1/reports/${id}/upvote`, {
method: 'POST', method: 'POST',
headers: AUTHORIZATION_HEADERS,
}) })
.then(response => response.json()) .then(response => response.json())
.then(response => response.data) .then(response => response.data)