From e3999c4906348dadcc85eec1df9a48e776deccd5 Mon Sep 17 00:00:00 2001 From: "Kali Kaneko (leap communications)" Date: Thu, 23 Feb 2017 00:35:33 +0100 Subject: [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 --- docs/core/index.rst | 27 +++++++++++++++++++++++++-- src/leap/bitmask/core/service.py | 18 ++++++++++++++++++ src/leap/bitmask/core/web/_auth.py | 7 ++++--- src/leap/bitmask/core/web/api.py | 11 ++++++++++- src/leap/bitmask/core/web/service.py | 20 ++++++-------------- src/leap/bitmask/gui/app.py | 10 +++++++--- tests/unit/core/test_web_api.py | 5 +++-- 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) -- cgit v1.2.3