summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKali Kaneko (leap communications) <kali@leap.se>2017-02-23 00:35:33 +0100
committerKali Kaneko (leap communications) <kali@leap.se>2017-02-24 16:20:52 +0100
commite3999c4906348dadcc85eec1df9a48e776deccd5 (patch)
tree7f8156ba80f367df22c4e823c301360706e06e8d
parent6b3ea883a62d40f8e2d68ce95bbefa2ac64b95de (diff)
[feature] require authentication token for api
implements a global auth token for the app. this token is written to .config/leap/authtoken, and passed to the anchor part of the landing URI when opening the index resource by the browser. - Resolves: #8765
-rw-r--r--docs/core/index.rst27
-rw-r--r--src/leap/bitmask/core/service.py18
-rw-r--r--src/leap/bitmask/core/web/_auth.py7
-rw-r--r--src/leap/bitmask/core/web/api.py11
-rw-r--r--src/leap/bitmask/core/web/service.py20
-rw-r--r--src/leap/bitmask/gui/app.py10
-rw-r--r--tests/unit/core/test_web_api.py5
7 files changed, 73 insertions, 25 deletions
diff --git a/docs/core/index.rst b/docs/core/index.rst
index d03dd727..c7fb1780 100644
--- a/docs/core/index.rst
+++ b/docs/core/index.rst
@@ -24,8 +24,31 @@ throught a REST API. In bitmaskd.cfg::
[services]
web = True
-API Authentication
-==================
+
+Global API Authentication
+=========================
+
+To avoid some kind of attacks, the Bitmask API is protected by a global
+authentication token.
+
+The JS API receives this value when the initial entrypoint is loaded for the
+first time, in the anchor part of the url.
+
+To authenticate any request to the API, the ``X-Bitmask-Auth`` header has to be
+added to it, set to the single value that is initialized during the bitmask
+deaemon startup::
+
+ curl -X POST http://localhost:7070/API/mail/status
+ unauthorized:bad auth token
+
+ curl -X POST http://localhost:7070/API/mail/status -H 'X-Bitmask-Auth: fae20706aa4f4f98ac0e67996787a370'
+ {"result": {"status": "on", "childrenStatus": {"smtp": {"status": "on", "error": null}, "imap": {"status": "on", "error": null}}, "error": null}, "error": null}
+
+This token can be found in ``.config/leap/authtoken``
+
+
+API Authentication (this section not implemented yet)
+======================================================
By default, the resources in the API are protected by an authentication token.
diff --git a/src/leap/bitmask/core/service.py b/src/leap/bitmask/core/service.py
index 902bfa6b..c06a5343 100644
--- a/src/leap/bitmask/core/service.py
+++ b/src/leap/bitmask/core/service.py
@@ -18,6 +18,8 @@
Bitmask-core Service.
"""
import json
+import os
+import uuid
try:
import resource
except ImportError:
@@ -62,6 +64,16 @@ class BitmaskBackend(configurable.ConfigurableService):
configurable.ConfigurableService.__init__(self, basedir)
self.core_commands = BackendCommands(self)
+
+ # The global token is used for authenticating some of the channels that
+ # expose the dispatcher. For the moment being, this is the REST API.
+ self.global_tokens = [uuid.uuid4().hex]
+ logger.info('Global token: {0}'.format(self.global_tokens[0]))
+ self._touch_token_file()
+
+ # These tokens are user-session tokens. Implemented and rolled back,
+ # unused for now. If we don't move forward with user-session tokens on
+ # top of the global app token, this should be removed.
self.tokens = {}
def enabled(service):
@@ -89,6 +101,12 @@ class BitmaskBackend(configurable.ConfigurableService):
if enabled('websockets'):
on_start(self._init_websockets)
+ def _touch_token_file(self):
+ path = os.path.join(self.basedir, 'authtoken')
+ with open(path, 'w') as f:
+ f.write(self.global_tokens[0])
+ os.chmod(path, 0600)
+
def init_events(self):
event_server.ensure_server()
diff --git a/src/leap/bitmask/core/web/_auth.py b/src/leap/bitmask/core/web/_auth.py
index 2747fae8..aa6aeb9b 100644
--- a/src/leap/bitmask/core/web/_auth.py
+++ b/src/leap/bitmask/core/web/_auth.py
@@ -6,6 +6,7 @@ from twisted.web.guard import HTTPAuthSessionWrapper, BasicCredentialFactory
from twisted.web.resource import IResource
+# Deprecate if the user-session tokens are finally not used.
class TokenCredentialFactory(BasicCredentialFactory):
scheme = 'token'
@@ -37,11 +38,11 @@ class WhitelistHTTPAuthSessionWrapper(HTTPAuthSessionWrapper):
return HTTPAuthSessionWrapper.render(self, request)
-def protectedResourceFactory(resource, session_tokens, whitelist):
+def protectedResourceFactory(resource, tokens, whitelist):
realm = HttpPasswordRealm(resource)
- checker = TokenDictChecker(session_tokens)
- resource_portal = portal.Portal(realm, [checker])
+ checker = TokenDictChecker(tokens)
credentialFactory = TokenCredentialFactory('localhost')
+ resource_portal = portal.Portal(realm, [checker])
protected_resource = WhitelistHTTPAuthSessionWrapper(
resource_portal, [credentialFactory],
whitelist=whitelist)
diff --git a/src/leap/bitmask/core/web/api.py b/src/leap/bitmask/core/web/api.py
index d31afa50..01c65bae 100644
--- a/src/leap/bitmask/core/web/api.py
+++ b/src/leap/bitmask/core/web/api.py
@@ -11,11 +11,20 @@ class Api(Resource):
isLeaf = True
- def __init__(self, dispatcher):
+ def __init__(self, dispatcher, global_tokens):
Resource.__init__(self)
self.dispatcher = dispatcher
+ self.global_tokens = global_tokens
def render_POST(self, request):
+ token = request.getHeader('x-bitmask-auth')
+ if not token:
+ request.setResponseCode(401)
+ return 'unauthorized: no app token'
+ elif token.strip() not in self.global_tokens:
+ request.setResponseCode(401)
+ return 'unauthorized: bad app token'
+
command = request.uri.split('/')[2:]
params = request.content.getvalue()
if params:
diff --git a/src/leap/bitmask/core/web/service.py b/src/leap/bitmask/core/web/service.py
index c1d839e8..2b8a7343 100644
--- a/src/leap/bitmask/core/web/service.py
+++ b/src/leap/bitmask/core/web/service.py
@@ -61,11 +61,6 @@ class HTTPDispatcherService(service.Service):
API_WHITELIST = (
'/API/core/version',
'/API/core/stats',
- '/API/bonafide/user/create',
- '/API/bonafide/user/authenticate',
- '/API/bonafide/provider/list',
- '/API/bonafide/provider/create',
- '/API/bonafide/provider/read',
)
def __init__(self, core, port=7070, debug=False, onion=False):
@@ -76,7 +71,6 @@ class HTTPDispatcherService(service.Service):
self.uri = ''
def startService(self):
- # TODO refactor this, too long----------------------------------------
if HAS_WEB_UI:
webdir = os.path.abspath(
pkg_resources.resource_filename('leap.bitmask_js', 'public'))
@@ -91,18 +85,16 @@ class HTTPDispatcherService(service.Service):
'ui', 'app', 'lib', 'bitmask.js')
jsapi = File(os.path.abspath(jspath))
- api = Api(CommandDispatcher(self._core))
- # protected_api = protectedResourceFactory(
- # api, self._core.tokens, self.API_WHITELIST)
+ api = Api(CommandDispatcher(self._core), self._core.global_tokens)
root = File(webdir)
+ root.putChild(u'API', api)
- # FIXME -- switching off the protected api, due to
- # https://0xacab.org/leap/bitmask-dev/issues/9
+ # XXX remove it we don't bring session tokens again
+ # protected_api = protectedResourceFactory(
+ # api, self._core.global_tokens, self.API_WHITELIST)
# root.putChild(u'API', protected_api)
- # -------------------------------------------------
- root.putChild(u'API', api)
if not HAS_WEB_UI:
root.putChild('bitmask.js', jsapi)
@@ -110,7 +102,7 @@ class HTTPDispatcherService(service.Service):
self.site = factory
if self.onion and _has_txtorcon():
- self._start_onion_service(factory)
+ self._start_onion_service(factory)
else:
interface = '127.0.0.1'
endpoint = endpoints.TCP4ServerEndpoint(
diff --git a/src/leap/bitmask/gui/app.py b/src/leap/bitmask/gui/app.py
index ce9fc880..14025afc 100644
--- a/src/leap/bitmask/gui/app.py
+++ b/src/leap/bitmask/gui/app.py
@@ -30,8 +30,8 @@ from functools import partial
from multiprocessing import Process
from leap.bitmask.core.launcher import run_bitmaskd, pid
-
from leap.bitmask.gui import app_rc
+from leap.common.config import get_path_prefix
if platform.system() == 'Windows':
@@ -51,7 +51,7 @@ else:
from PyQt5.QtCore import QSize
-BITMASK_URI = 'http://localhost:7070'
+BITMASK_URI = 'http://localhost:7070/'
IS_WIN = platform.system() == "Windows"
DEBUG = os.environ.get("DEBUG", False)
@@ -100,7 +100,11 @@ class BrowserWindow(QDialog):
self.closing = False
def load_app(self):
- self.view.load(QtCore.QUrl(BITMASK_URI))
+ path = os.path.join(get_path_prefix(), 'leap', 'authtoken')
+ global_token = open(path).read().strip()
+ anchored_uri = BITMASK_URI + 'index.html#' + global_token
+ print('[bitmask] opening Browser with {0}'.format(anchored_uri))
+ self.view.load(QtCore.QUrl(anchored_uri))
def shutdown(self, *args):
if self.closing:
diff --git a/tests/unit/core/test_web_api.py b/tests/unit/core/test_web_api.py
index 5b51869f..10356f02 100644
--- a/tests/unit/core/test_web_api.py
+++ b/tests/unit/core/test_web_api.py
@@ -131,7 +131,7 @@ class RESTApiTests(unittest.TestCase):
def setUp(self):
dispatcher = dummyDispatcherFactory()
- api = web.api.Api(dispatcher)
+ api = web.api.Api(dispatcher, ['aaa'])
root = resource.Resource()
root.putChild(b"API", api)
plainSite = Site(root)
@@ -205,7 +205,8 @@ class RESTApiTests(unittest.TestCase):
uri = networkString("http://127.0.0.1:%d/API/%s" % (
self.plainPortno, path))
return client.getPage(
- uri, method=method, timeout=1, postdata=postdata)
+ uri, method=method, timeout=1, postdata=postdata,
+ headers={'X-Bitmask-Auth': 'aaa'})
def assertCall(self, returned, expected):
data = json.loads(returned)