]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Use secure cookies to store JWT Token 38259/head
authorAashish Sharma <aashishsharma@localhost.localdomain>
Tue, 24 Nov 2020 05:58:28 +0000 (11:28 +0530)
committerAvan Thakkar <athakkar@localhost.localdomain>
Fri, 18 Dec 2020 14:03:50 +0000 (19:33 +0530)
This PR intends to store the jwt token in secure cookies instead of local storage

Fixes: https://tracker.ceph.com/issues/44591
Signed-off-by: Aashish Sharma <aasharma@redhat.com>
Signed-off-by: Avan Thakkar <athakkar@redhat.com>
(cherry picked from commit 36703c63381e6723fff57266235f8230e6af1d92)

44 files changed:
qa/tasks/mgr/dashboard/helper.py
qa/tasks/mgr/dashboard/test_auth.py
src/pybind/mgr/dashboard/controllers/__init__.py
src/pybind/mgr/dashboard/controllers/auth.py
src/pybind/mgr/dashboard/controllers/docs.py
src/pybind/mgr/dashboard/controllers/saml2.py
src/pybind/mgr/dashboard/frontend/cypress/integration/block/images.e2e-spec.ts
src/pybind/mgr/dashboard/frontend/cypress/integration/block/iscsi.e2e-spec.ts
src/pybind/mgr/dashboard/frontend/cypress/integration/block/mirroring.e2e-spec.ts
src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/configuration.e2e-spec.ts
src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/crush-map.e2e-spec.ts
src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/hosts.e2e-spec.ts
src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/logs.e2e-spec.ts
src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/mgr-modules.e2e-spec.ts
src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/monitors.e2e-spec.ts
src/pybind/mgr/dashboard/frontend/cypress/integration/cluster/osds.e2e-spec.ts
src/pybind/mgr/dashboard/frontend/cypress/integration/filesystems/filesystems.e2e-spec.ts
src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/01-hosts.e2e-spec.ts
src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/02-hosts-inventory.e2e-spec.ts
src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/03-inventory.e2e-spec.ts
src/pybind/mgr/dashboard/frontend/cypress/integration/orchestrator/04-osds.e2e-spec.ts
src/pybind/mgr/dashboard/frontend/cypress/integration/pools/pools.e2e-spec.ts
src/pybind/mgr/dashboard/frontend/cypress/integration/rgw/buckets.e2e-spec.ts
src/pybind/mgr/dashboard/frontend/cypress/integration/rgw/daemons.e2e-spec.ts
src/pybind/mgr/dashboard/frontend/cypress/integration/rgw/users.e2e-spec.ts
src/pybind/mgr/dashboard/frontend/cypress/integration/ui/dashboard.e2e-spec.ts
src/pybind/mgr/dashboard/frontend/cypress/integration/ui/language.e2e-spec.ts
src/pybind/mgr/dashboard/frontend/cypress/integration/ui/navigation.e2e-spec.ts
src/pybind/mgr/dashboard/frontend/cypress/integration/ui/notification.e2e-spec.ts
src/pybind/mgr/dashboard/frontend/cypress/integration/ui/role-mgmt.e2e-spec.ts
src/pybind/mgr/dashboard/frontend/cypress/integration/ui/user-mgmt.e2e-spec.ts
src/pybind/mgr/dashboard/frontend/cypress/support/commands.ts
src/pybind/mgr/dashboard/frontend/package-lock.json
src/pybind/mgr/dashboard/frontend/package.json
src/pybind/mgr/dashboard/frontend/src/app/app.module.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/core/auth/login/login.component.ts
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/dashboard-help/dashboard-help.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/auth.service.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/auth.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/models/login-response.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/services/auth-storage.service.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/services/auth-storage.service.ts
src/pybind/mgr/dashboard/services/auth.py

index bbf7eb4d5f45d38caa2b6175d79672c56f5c94cd..78915eb201527bf8abb17d81078f4aec38131978 100644 (file)
@@ -157,18 +157,19 @@ class DashboardTestCase(MgrTestCase):
         cls._task_post("/api/pool", data)
 
     @classmethod
-    def login(cls, username, password):
+    def login(cls, username, password, set_cookies=False):
         if cls._loggedin:
             cls.logout()
-        cls._post('/api/auth', {'username': username, 'password': password})
+        cls._post('/api/auth', {'username': username,
+                                'password': password}, set_cookies=set_cookies)
         cls._assertEq(cls._resp.status_code, 201)
         cls._token = cls.jsonBody()['token']
         cls._loggedin = True
 
     @classmethod
-    def logout(cls):
+    def logout(cls, set_cookies=False):
         if cls._loggedin:
-            cls._post('/api/auth/logout')
+            cls._post('/api/auth/logout', set_cookies=set_cookies)
             cls._assertEq(cls._resp.status_code, 200)
             cls._token = None
             cls._loggedin = False
@@ -273,33 +274,54 @@ class DashboardTestCase(MgrTestCase):
     def tearDownClass(cls):
         super(DashboardTestCase, cls).tearDownClass()
 
-    # pylint: disable=inconsistent-return-statements, too-many-arguments
+    # pylint: disable=inconsistent-return-statements, too-many-arguments, too-many-branches
     @classmethod
-    def _request(cls, url, method, data=None, params=None, version=DEFAULT_VERSION):
+    def _request(cls, url, method, data=None, params=None, version=DEFAULT_VERSION,
+                 set_cookies=False):
         url = "{}{}".format(cls._base_uri, url)
         log.info("Request %s to %s", method, url)
         headers = {}
+        cookies = {}
         if cls._token:
-            headers['Authorization'] = "Bearer {}".format(cls._token)
-
+            if set_cookies:
+                cookies['token'] = cls._token
+            else:
+                headers['Authorization'] = "Bearer {}".format(cls._token)
         if version is None:
             headers['Accept'] = 'application/json'
         else:
             headers['Accept'] = 'application/vnd.ceph.api.v{}+json'.format(version)
-        if method == 'GET':
-            cls._resp = cls._session.get(url, params=params, verify=False,
-                                         headers=headers)
-        elif method == 'POST':
-            cls._resp = cls._session.post(url, json=data, params=params,
-                                          verify=False, headers=headers)
-        elif method == 'DELETE':
-            cls._resp = cls._session.delete(url, json=data, params=params,
-                                            verify=False, headers=headers)
-        elif method == 'PUT':
-            cls._resp = cls._session.put(url, json=data, params=params,
-                                         verify=False, headers=headers)
+
+        if set_cookies:
+            if method == 'GET':
+                cls._resp = cls._session.get(url, params=params, verify=False,
+                                             headers=headers, cookies=cookies)
+            elif method == 'POST':
+                cls._resp = cls._session.post(url, json=data, params=params,
+                                              verify=False, headers=headers, cookies=cookies)
+            elif method == 'DELETE':
+                cls._resp = cls._session.delete(url, json=data, params=params,
+                                                verify=False, headers=headers, cookies=cookies)
+            elif method == 'PUT':
+                cls._resp = cls._session.put(url, json=data, params=params,
+                                             verify=False, headers=headers, cookies=cookies)
+            else:
+                assert False
         else:
-            assert False
+            if method == 'GET':
+                cls._resp = cls._session.get(url, params=params, verify=False,
+                                             headers=headers)
+            elif method == 'POST':
+                cls._resp = cls._session.post(url, json=data, params=params,
+                                              verify=False, headers=headers)
+            elif method == 'DELETE':
+                cls._resp = cls._session.delete(url, json=data, params=params,
+                                                verify=False, headers=headers)
+            elif method == 'PUT':
+                cls._resp = cls._session.put(url, json=data, params=params,
+                                             verify=False, headers=headers)
+            else:
+                assert False
         try:
             if not cls._resp.ok:
                 # Output response for easier debugging.
@@ -314,8 +336,8 @@ class DashboardTestCase(MgrTestCase):
             raise ex
 
     @classmethod
-    def _get(cls, url, params=None, version=DEFAULT_VERSION):
-        return cls._request(url, 'GET', params=params, version=version)
+    def _get(cls, url, params=None, version=DEFAULT_VERSION, set_cookies=False):
+        return cls._request(url, 'GET', params=params, version=version, set_cookies=set_cookies)
 
     @classmethod
     def _view_cache_get(cls, url, retries=5):
@@ -336,16 +358,16 @@ class DashboardTestCase(MgrTestCase):
         return res
 
     @classmethod
-    def _post(cls, url, data=None, params=None, version=DEFAULT_VERSION):
-        cls._request(url, 'POST', data, params, version=version)
+    def _post(cls, url, data=None, params=None, version=DEFAULT_VERSION, set_cookies=False):
+        cls._request(url, 'POST', data, params, version=version, set_cookies=set_cookies)
 
     @classmethod
-    def _delete(cls, url, data=None, params=None, version=DEFAULT_VERSION):
-        cls._request(url, 'DELETE', data, params, version=version)
+    def _delete(cls, url, data=None, params=None, version=DEFAULT_VERSION, set_cookies=False):
+        cls._request(url, 'DELETE', data, params, version=version, set_cookies=set_cookies)
 
     @classmethod
-    def _put(cls, url, data=None, params=None, version=DEFAULT_VERSION):
-        cls._request(url, 'PUT', data, params, version=version)
+    def _put(cls, url, data=None, params=None, version=DEFAULT_VERSION, set_cookies=False):
+        cls._request(url, 'PUT', data, params, version=version, set_cookies=set_cookies)
 
     @classmethod
     def _assertEq(cls, v1, v2):
@@ -364,8 +386,8 @@ class DashboardTestCase(MgrTestCase):
 
     # pylint: disable=too-many-arguments
     @classmethod
-    def _task_request(cls, method, url, data, timeout, version=DEFAULT_VERSION):
-        res = cls._request(url, method, data, version=version)
+    def _task_request(cls, method, url, data, timeout, version=DEFAULT_VERSION, set_cookies=False):
+        res = cls._request(url, method, data, version=version, set_cookies=set_cookies)
         cls._assertIn(cls._resp.status_code, [200, 201, 202, 204, 400, 403, 404])
 
         if cls._resp.status_code == 403:
@@ -417,16 +439,19 @@ class DashboardTestCase(MgrTestCase):
         return res_task['exception']
 
     @classmethod
-    def _task_post(cls, url, data=None, timeout=60, version=DEFAULT_VERSION):
-        return cls._task_request('POST', url, data, timeout, version=version)
+    def _task_post(cls, url, data=None, timeout=60, version=DEFAULT_VERSION, set_cookies=False):
+        return cls._task_request('POST', url, data, timeout, version=version,
+                                 set_cookies=set_cookies)
 
     @classmethod
-    def _task_delete(cls, url, timeout=60, version=DEFAULT_VERSION):
-        return cls._task_request('DELETE', url, None, timeout, version=version)
+    def _task_delete(cls, url, timeout=60, version=DEFAULT_VERSION, set_cookies=False):
+        return cls._task_request('DELETE', url, None, timeout, version=version,
+                                 set_cookies=set_cookies)
 
     @classmethod
-    def _task_put(cls, url, data=None, timeout=60, version=DEFAULT_VERSION):
-        return cls._task_request('PUT', url, data, timeout, version=version)
+    def _task_put(cls, url, data=None, timeout=60, version=DEFAULT_VERSION, set_cookies=False):
+        return cls._task_request('PUT', url, data, timeout, version=version,
+                                 set_cookies=set_cookies)
 
     @classmethod
     def cookies(cls):
index 9bc195bbbbdf994fe30a5bbc2201c40bc14756c5..ca7a0cd82296912757a37b4ed843b4db7e9e4b80 100644 (file)
@@ -36,6 +36,7 @@ class AuthTest(DashboardTestCase):
             self.create_user('admin2', '', ['administrator'], force_password=True)
 
     def test_a_set_login_credentials(self):
+        # test with Authorization header
         self.create_user('admin2', 'admin2', ['administrator'])
         self._post("/api/auth", {'username': 'admin2', 'password': 'admin2'})
         self.assertStatus(201)
@@ -43,7 +44,16 @@ class AuthTest(DashboardTestCase):
         self._validate_jwt_token(data['token'], "admin2", data['permissions'])
         self.delete_user('admin2')
 
+        # test with Cookies set
+        self.create_user('admin2', 'admin2', ['administrator'])
+        self._post("/api/auth", {'username': 'admin2', 'password': 'admin2'}, set_cookies=True)
+        self.assertStatus(201)
+        data = self.jsonBody()
+        self._validate_jwt_token(data['token'], "admin2", data['permissions'])
+        self.delete_user('admin2')
+
     def test_login_valid(self):
+        # test with Authorization header
         self._post("/api/auth", {'username': 'admin', 'password': 'admin'})
         self.assertStatus(201)
         data = self.jsonBody()
@@ -57,7 +67,22 @@ class AuthTest(DashboardTestCase):
         }, allow_unknown=False))
         self._validate_jwt_token(data['token'], "admin", data['permissions'])
 
+        # test with Cookies set
+        self._post("/api/auth", {'username': 'admin', 'password': 'admin'}, set_cookies=True)
+        self.assertStatus(201)
+        data = self.jsonBody()
+        self.assertSchema(data, JObj(sub_elems={
+            'token': JLeaf(str),
+            'username': JLeaf(str),
+            'permissions': JObj(sub_elems={}, allow_unknown=True),
+            'sso': JLeaf(bool),
+            'pwdExpirationDate': JLeaf(int, none=True),
+            'pwdUpdateRequired': JLeaf(bool)
+        }, allow_unknown=False))
+        self._validate_jwt_token(data['token'], "admin", data['permissions'])
+
     def test_login_invalid(self):
+        # test with Authorization header
         self._post("/api/auth", {'username': 'admin', 'password': 'inval'})
         self.assertStatus(400)
         self.assertJsonBody({
@@ -67,6 +92,7 @@ class AuthTest(DashboardTestCase):
         })
 
     def test_lockout_user(self):
+        # test with Authorization header
         self._ceph_cmd(['dashboard', 'set-account-lockout-attempts', '3'])
         for _ in range(3):
             self._post("/api/auth", {'username': 'admin', 'password': 'inval'})
@@ -91,7 +117,33 @@ class AuthTest(DashboardTestCase):
         }, allow_unknown=False))
         self._validate_jwt_token(data['token'], "admin", data['permissions'])
 
+        # test with Cookies set
+        self._ceph_cmd(['dashboard', 'set-account-lockout-attempts', '3'])
+        for _ in range(3):
+            self._post("/api/auth", {'username': 'admin', 'password': 'inval'}, set_cookies=True)
+        self._post("/api/auth", {'username': 'admin', 'password': 'admin'}, set_cookies=True)
+        self.assertStatus(400)
+        self.assertJsonBody({
+            "component": "auth",
+            "code": "invalid_credentials",
+            "detail": "Invalid credentials"
+        })
+        self._ceph_cmd(['dashboard', 'ac-user-enable', 'admin'])
+        self._post("/api/auth", {'username': 'admin', 'password': 'admin'}, set_cookies=True)
+        self.assertStatus(201)
+        data = self.jsonBody()
+        self.assertSchema(data, JObj(sub_elems={
+            'token': JLeaf(str),
+            'username': JLeaf(str),
+            'permissions': JObj(sub_elems={}, allow_unknown=True),
+            'sso': JLeaf(bool),
+            'pwdExpirationDate': JLeaf(int, none=True),
+            'pwdUpdateRequired': JLeaf(bool)
+        }, allow_unknown=False))
+        self._validate_jwt_token(data['token'], "admin", data['permissions'])
+
     def test_logout(self):
+        # test with Authorization header
         self._post("/api/auth", {'username': 'admin', 'password': 'admin'})
         self.assertStatus(201)
         data = self.jsonBody()
@@ -106,7 +158,23 @@ class AuthTest(DashboardTestCase):
         self.assertStatus(401)
         self.set_jwt_token(None)
 
+        # test with Cookies set
+        self._post("/api/auth", {'username': 'admin', 'password': 'admin'}, set_cookies=True)
+        self.assertStatus(201)
+        data = self.jsonBody()
+        self._validate_jwt_token(data['token'], "admin", data['permissions'])
+        self.set_jwt_token(data['token'])
+        self._post("/api/auth/logout", set_cookies=True)
+        self.assertStatus(200)
+        self.assertJsonBody({
+            "redirect_url": "#/login"
+        })
+        self._get("/api/host", set_cookies=True)
+        self.assertStatus(401)
+        self.set_jwt_token(None)
+
     def test_token_ttl(self):
+        # test with Authorization header
         self._ceph_cmd(['dashboard', 'set-jwt-token-ttl', '5'])
         self._post("/api/auth", {'username': 'admin', 'password': 'admin'})
         self.assertStatus(201)
@@ -119,7 +187,21 @@ class AuthTest(DashboardTestCase):
         self._ceph_cmd(['dashboard', 'set-jwt-token-ttl', '28800'])
         self.set_jwt_token(None)
 
+        # test with Cookies set
+        self._ceph_cmd(['dashboard', 'set-jwt-token-ttl', '5'])
+        self._post("/api/auth", {'username': 'admin', 'password': 'admin'}, set_cookies=True)
+        self.assertStatus(201)
+        self.set_jwt_token(self.jsonBody()['token'])
+        self._get("/api/host", set_cookies=True)
+        self.assertStatus(200)
+        time.sleep(6)
+        self._get("/api/host", set_cookies=True)
+        self.assertStatus(401)
+        self._ceph_cmd(['dashboard', 'set-jwt-token-ttl', '28800'])
+        self.set_jwt_token(None)
+
     def test_remove_from_blocklist(self):
+        # test with Authorization header
         self._ceph_cmd(['dashboard', 'set-jwt-token-ttl', '5'])
         self._post("/api/auth", {'username': 'admin', 'password': 'admin'})
         self.assertStatus(201)
@@ -139,11 +221,37 @@ class AuthTest(DashboardTestCase):
         self._post("/api/auth/logout")
         self.assertStatus(200)
 
+        # test with Cookies set
+        self._ceph_cmd(['dashboard', 'set-jwt-token-ttl', '5'])
+        self._post("/api/auth", {'username': 'admin', 'password': 'admin'}, set_cookies=True)
+        self.assertStatus(201)
+        self.set_jwt_token(self.jsonBody()['token'])
+        # the following call adds the token to the blocklist
+        self._post("/api/auth/logout", set_cookies=True)
+        self.assertStatus(200)
+        self._get("/api/host", set_cookies=True)
+        self.assertStatus(401)
+        time.sleep(6)
+        self._ceph_cmd(['dashboard', 'set-jwt-token-ttl', '28800'])
+        self.set_jwt_token(None)
+        self._post("/api/auth", {'username': 'admin', 'password': 'admin'}, set_cookies=True)
+        self.assertStatus(201)
+        self.set_jwt_token(self.jsonBody()['token'])
+        # the following call removes expired tokens from the blocklist
+        self._post("/api/auth/logout", set_cookies=True)
+        self.assertStatus(200)
+
     def test_unauthorized(self):
+        # test with Authorization header
         self._get("/api/host")
         self.assertStatus(401)
 
+        # test with Cookies set
+        self._get("/api/host", set_cookies=True)
+        self.assertStatus(401)
+
     def test_invalidate_token_by_admin(self):
+        # test with Authorization header
         self._get("/api/host")
         self.assertStatus(401)
         self.create_user('user', 'user', ['read-only'])
@@ -168,7 +276,33 @@ class AuthTest(DashboardTestCase):
         self.assertStatus(200)
         self.delete_user("user")
 
+        # test with Cookies set
+        self._get("/api/host", set_cookies=True)
+        self.assertStatus(401)
+        self.create_user('user', 'user', ['read-only'])
+        time.sleep(1)
+        self._post("/api/auth", {'username': 'user', 'password': 'user'}, set_cookies=True)
+        self.assertStatus(201)
+        self.set_jwt_token(self.jsonBody()['token'])
+        self._get("/api/host", set_cookies=True)
+        self.assertStatus(200)
+        time.sleep(1)
+        self._ceph_cmd_with_secret(['dashboard', 'ac-user-set-password', '--force-password',
+                                    'user'],
+                                   'user2')
+        time.sleep(1)
+        self._get("/api/host", set_cookies=True)
+        self.assertStatus(401)
+        self.set_jwt_token(None)
+        self._post("/api/auth", {'username': 'user', 'password': 'user2'}, set_cookies=True)
+        self.assertStatus(201)
+        self.set_jwt_token(self.jsonBody()['token'])
+        self._get("/api/host", set_cookies=True)
+        self.assertStatus(200)
+        self.delete_user("user")
+
     def test_check_token(self):
+        # test with Authorization header
         self.login("admin", "admin")
         self._post("/api/auth/check", {"token": self.jsonBody()["token"]})
         self.assertStatus(200)
@@ -181,7 +315,21 @@ class AuthTest(DashboardTestCase):
         }, allow_unknown=False))
         self.logout()
 
+        # test with Cookies set
+        self.login("admin", "admin", set_cookies=True)
+        self._post("/api/auth/check", {"token": self.jsonBody()["token"]}, set_cookies=True)
+        self.assertStatus(200)
+        data = self.jsonBody()
+        self.assertSchema(data, JObj(sub_elems={
+            "username": JLeaf(str),
+            "permissions": JObj(sub_elems={}, allow_unknown=True),
+            "sso": JLeaf(bool),
+            "pwdUpdateRequired": JLeaf(bool)
+        }, allow_unknown=False))
+        self.logout(set_cookies=True)
+
     def test_check_wo_token(self):
+        # test with Authorization header
         self.login("admin", "admin")
         self._post("/api/auth/check", {"token": ""})
         self.assertStatus(200)
@@ -190,3 +338,13 @@ class AuthTest(DashboardTestCase):
             "login_url": JLeaf(str)
         }, allow_unknown=False))
         self.logout()
+
+        # test with Cookies set
+        self.login("admin", "admin", set_cookies=True)
+        self._post("/api/auth/check", {"token": ""}, set_cookies=True)
+        self.assertStatus(200)
+        data = self.jsonBody()
+        self.assertSchema(data, JObj(sub_elems={
+            "login_url": JLeaf(str)
+        }, allow_unknown=False))
+        self.logout(set_cookies=True)
index 2f3c186b952d502eb7b06c3db4f0334216ac4cba..11b6323255c2e33336c46999a9e964d6d90fdcbd 100644 (file)
@@ -1031,3 +1031,12 @@ def validate_ceph_type(validations, component=''):
             return func(*args, **kwargs)
         return validate_args
     return decorator
+
+
+def set_cookies(url_prefix, token):
+    cherrypy.response.cookie['token'] = token
+    if url_prefix == 'https':
+        cherrypy.response.cookie['token']['secure'] = True
+    cherrypy.response.cookie['token']['HttpOnly'] = True
+    cherrypy.response.cookie['token']['path'] = '/'
+    cherrypy.response.cookie['token']['SameSite'] = 'Strict'
index 264204d557a5d112bd7e492f21ebea5dfbb686df..cd50006e28d463446511fadb96d8574696f54ac9 100644 (file)
@@ -1,15 +1,21 @@
 # -*- coding: utf-8 -*-
 from __future__ import absolute_import
 
+import http.cookies
 import logging
-
-import cherrypy
+import sys
 
 from .. import mgr
 from ..exceptions import InvalidCredentialsError, UserDoesNotExist
 from ..services.auth import AuthManager, JwtManager
 from ..settings import Settings
-from . import ApiController, ControllerDoc, EndpointDoc, RESTController, allow_empty_body
+from . import ApiController, ControllerDoc, EndpointDoc, RESTController, \
+    allow_empty_body, set_cookies
+
+# Python 3.8 introduced `samesite` attribute:
+# https://docs.python.org/3/library/http.cookies.html#morsel-objects
+if sys.version_info < (3, 8):
+    http.cookies.Morsel._reserved["samesite"] = "SameSite"  # type: ignore  # pylint: disable=W0212
 
 logger = logging.getLogger('controllers.auth')
 
@@ -40,12 +46,14 @@ class Auth(RESTController):
                 pwd_update_required = user_data.get('pwdUpdateRequired', False)
 
             if user_perms is not None:
+                url_prefix = 'https' if mgr.get_localized_module_option('ssl') else 'http'
+
                 logger.info('Login successful: %s', username)
                 mgr.ACCESS_CTRL_DB.reset_attempt(username)
                 mgr.ACCESS_CTRL_DB.save()
                 token = JwtManager.gen_token(username)
                 token = token.decode('utf-8')
-                cherrypy.response.headers['Authorization'] = "Bearer: {}".format(token)
+                set_cookies(url_prefix, token)
                 return {
                     'token': token,
                     'username': username,
index f4ff08ed7861d4a5b3cd7aebfaeacadffe9c0ccf..295a36ad85594832214161416139e27d2b6c67ba 100644 (file)
@@ -391,8 +391,11 @@ class Docs(BaseController):
             spec_url = "{}/docs/api.json".format(base)
 
         auth_header = cherrypy.request.headers.get('authorization')
+        auth_cookie = cherrypy.request.cookie['token']
         jwt_token = ""
-        if auth_header is not None:
+        if auth_cookie is not None:
+            jwt_token = auth_cookie.value
+        elif auth_header is not None:
             scheme, params = auth_header.split(' ', 1)
             if scheme.lower() == 'bearer':
                 jwt_token = params
index 76a7e193a9ac1d2a0ac2d24bd5134cd0271e2eb9..84e8a132a0bc25cde3bf1ab49420640e03abf8ed 100644 (file)
@@ -16,7 +16,7 @@ from .. import mgr
 from ..exceptions import UserDoesNotExist
 from ..services.auth import JwtManager
 from ..tools import prepare_url_prefix
-from . import BaseController, Controller, Endpoint, allow_empty_body
+from . import BaseController, Controller, Endpoint, allow_empty_body, set_cookies
 
 
 @Controller('/auth/saml2', secure=False)
@@ -71,6 +71,7 @@ class Saml2(BaseController):
             token = JwtManager.gen_token(username)
             JwtManager.set_user(JwtManager.decode_token(token))
             token = token.decode('utf-8')
+            set_cookies(url_prefix, token)
             raise cherrypy.HTTPRedirect("{}/#/login?access_token={}".format(url_prefix, token))
 
         return {
@@ -104,5 +105,6 @@ class Saml2(BaseController):
         # pylint: disable=unused-argument
         Saml2._check_python_saml()
         JwtManager.reset_user()
+        cherrypy.response.cookie['token'] = {'expires': 0, 'max-age': 0}
         url_prefix = prepare_url_prefix(mgr.get_module_option('url_prefix', default=''))
         raise cherrypy.HTTPRedirect("{}/#/login".format(url_prefix))
index 87900a0e1a5172ec9c7ad901b3d6dc57f43a802c..cf8832bb9bbc07ac98d10d67821facf87847d350 100644 (file)
@@ -9,6 +9,7 @@ describe('Images page', () => {
 
   before(() => {
     cy.login();
+    Cypress.Cookies.preserveOnce('token');
     // Need pool for image testing
     pools.navigateTo('create');
     pools.create(poolName, 8, 'rbd');
@@ -25,6 +26,7 @@ describe('Images page', () => {
 
   beforeEach(() => {
     cy.login();
+    Cypress.Cookies.preserveOnce('token');
     images.navigateTo();
   });
 
@@ -68,6 +70,7 @@ describe('Images page', () => {
 
     before(() => {
       cy.login();
+      Cypress.Cookies.preserveOnce('token');
       // Need image for trash testing
       images.createImage(imageName, poolName, '1');
       images.getFirstTableCell(imageName).should('exist');
index 2788c4f9b9de748809671a2e0f9ffb616a9620fb..cef4874bed50fbe8baf63464ca49273d3dbfbef6 100644 (file)
@@ -5,6 +5,7 @@ describe('Iscsi Page', () => {
 
   beforeEach(() => {
     cy.login();
+    Cypress.Cookies.preserveOnce('token');
     iscsi.navigateTo();
   });
 
index c7bd42782389ac2f49a3e7e91ada269cbf53a88f..ddee817e18ef1232baf21395347679455d739d63 100644 (file)
@@ -7,6 +7,7 @@ describe('Mirroring page', () => {
 
   beforeEach(() => {
     cy.login();
+    Cypress.Cookies.preserveOnce('token');
     mirroring.navigateTo();
   });
 
index 6c012166ebb1d48d9cbd7fa9470d86a4fd57e2b6..0acbc9100b8b08aa5e1f9a686cfe0b9c721ba593 100644 (file)
@@ -5,6 +5,7 @@ describe('Configuration page', () => {
 
   beforeEach(() => {
     cy.login();
+    Cypress.Cookies.preserveOnce('token');
     configuration.navigateTo();
   });
 
index 2c8d1322f4e9b3fd150eba45de95d932d660cbd0..2274d72e7bcb8138fc3b9910eeba71c60c53d4b9 100644 (file)
@@ -5,6 +5,7 @@ describe('CRUSH map page', () => {
 
   beforeEach(() => {
     cy.login();
+    Cypress.Cookies.preserveOnce('token');
     crushmap.navigateTo();
   });
 
index 045b18f60cdb1086f916137836126b14f6da8d5d..c1935a7838319940027b7b919e36cbc2db5979f1 100644 (file)
@@ -5,6 +5,7 @@ describe('Hosts page', () => {
 
   beforeEach(() => {
     cy.login();
+    Cypress.Cookies.preserveOnce('token');
     hosts.navigateTo();
   });
 
index f5692bfe15fa3c676faf9144d218552fbd1eb73b..731275e26d1c15a2d3812423b7218be19a962fac 100644 (file)
@@ -15,6 +15,7 @@ describe('Logs page', () => {
 
   beforeEach(() => {
     cy.login();
+    Cypress.Cookies.preserveOnce('token');
   });
 
   describe('breadcrumb and tab tests', () => {
index f163b0c230f8029f09e2fc41065b53c8e98cefe8..82c6878969935e787186961d320a55c4545a6536 100644 (file)
@@ -5,6 +5,7 @@ describe('Manager modules page', () => {
 
   beforeEach(() => {
     cy.login();
+    Cypress.Cookies.preserveOnce('token');
     mgrmodules.navigateTo();
   });
 
index 8324ff8b5b058505d56562e3dedd4912b2ab7491..a23d071e6d7213f0c9140c84ba7279b70be8b758 100644 (file)
@@ -5,6 +5,7 @@ describe('Monitors page', () => {
 
   beforeEach(() => {
     cy.login();
+    Cypress.Cookies.preserveOnce('token');
     monitors.navigateTo();
   });
 
index 647d4aadb472ef445c59fb146760a957b454bd69..e68d03bd542b0a6708b2c474f1965a0bf227e261 100644 (file)
@@ -5,6 +5,7 @@ describe('OSDs page', () => {
 
   beforeEach(() => {
     cy.login();
+    Cypress.Cookies.preserveOnce('token');
     osds.navigateTo();
   });
 
index 63f59916f9966e291fb1215c1b44c76dc64b81fc..83f280b477ac9097204d140ece13e74129849b20 100644 (file)
@@ -5,6 +5,7 @@ describe('Filesystems page', () => {
 
   beforeEach(() => {
     cy.login();
+    Cypress.Cookies.preserveOnce('token');
     filesystems.navigateTo();
   });
 
index e7609454a180ba0e616fb13281d2e2b6b55a89b6..3e7e6733312b21b4ee13dace929c85e8d20ba0e0 100644 (file)
@@ -5,6 +5,7 @@ describe('Hosts page', () => {
 
   beforeEach(() => {
     cy.login();
+    Cypress.Cookies.preserveOnce('token');
     hosts.navigateTo();
   });
 
index 3b74de223d35a3bdc110a9a106d21ebf460036f6..2b9ca548d543573e8a82d48c96760b851cf70220 100644 (file)
@@ -5,6 +5,7 @@ describe('Hosts page', () => {
 
   beforeEach(() => {
     cy.login();
+    Cypress.Cookies.preserveOnce('token');
     hosts.navigateTo();
   });
 
index 41dcdecde3fe29fb5c1cc604025872526aa6f948..596653ab5564789b04420f4e7407da59a99c6f24 100644 (file)
@@ -5,6 +5,7 @@ describe('Inventory page', () => {
 
   beforeEach(() => {
     cy.login();
+    Cypress.Cookies.preserveOnce('token');
     inventory.navigateTo();
   });
 
index e80398d5a47208840168e9f86f2613866bf75b47..41f0933b7a0b8e9ac4ea8918fb1c4d41723ccf10 100644 (file)
@@ -7,6 +7,7 @@ describe('OSDs page', () => {
 
   beforeEach(() => {
     cy.login();
+    Cypress.Cookies.preserveOnce('token');
     osds.navigateTo();
   });
 
index ff925b8a36e4cb77ac1317e845c92b326f06d000..e5a28bfd4e20c24c3dc8dec3f15497db4fac18d3 100644 (file)
@@ -6,6 +6,7 @@ describe('Pools page', () => {
 
   beforeEach(() => {
     cy.login();
+    Cypress.Cookies.preserveOnce('token');
     pools.navigateTo();
   });
 
index e5e0daa4c1f5da968a72e1b3a10cb97e27e74b51..737c112682153fc65018a0c1fe382cf1e44cbc55 100644 (file)
@@ -6,6 +6,7 @@ describe('RGW buckets page', () => {
 
   beforeEach(() => {
     cy.login();
+    Cypress.Cookies.preserveOnce('token');
     buckets.navigateTo();
   });
 
index 03b2ca8342198ed1aad2f47cceba1b991b190846..4cad786c6fefe412aff8584105650d38cbfedca9 100644 (file)
@@ -5,6 +5,7 @@ describe('RGW daemons page', () => {
 
   beforeEach(() => {
     cy.login();
+    Cypress.Cookies.preserveOnce('token');
     daemons.navigateTo();
   });
 
index a8d7d45b4190f1bb3651fa3c8f76ec94506bc0d4..1b580db7dcfc40e0037f41bbb99a3a98bd54593f 100644 (file)
@@ -6,6 +6,7 @@ describe('RGW users page', () => {
 
   beforeEach(() => {
     cy.login();
+    Cypress.Cookies.preserveOnce('token');
     users.navigateTo();
   });
 
index c33112be72e6be719256e5b428e938f68695a046..9cb84480b6432cdda5f48dedf6020d18b0d662e3 100644 (file)
@@ -18,6 +18,7 @@ describe('Dashboard Main Page', () => {
 
   beforeEach(() => {
     cy.login();
+    Cypress.Cookies.preserveOnce('token');
     dashboard.navigateTo();
   });
 
index fa20f0be542730f774e03b803e46c355454d7cbc..ccf16c2b55c78fb514fa79850d9210ccb0c5661b 100644 (file)
@@ -5,6 +5,7 @@ describe('Shared pages', () => {
 
   beforeEach(() => {
     cy.login();
+    Cypress.Cookies.preserveOnce('token');
     language.navigateTo();
   });
 
index f9d74a08b39d0750a83d286a2e33fd77df0c0f97..71bfa4b9f2452627f36885ed9644fd8b2c073e4d 100644 (file)
@@ -5,6 +5,7 @@ describe('Shared pages', () => {
 
   beforeEach(() => {
     cy.login();
+    Cypress.Cookies.preserveOnce('token');
     shared.navigateTo();
   });
 
index b69f26f58dc3818fc5ad61798d1832e1870dd1c9..2ee73a70632baf68494f905f65261eb9210c8900 100644 (file)
@@ -8,6 +8,7 @@ describe('Notification page', () => {
 
   before(() => {
     cy.login();
+    Cypress.Cookies.preserveOnce('token');
     pools.navigateTo('create');
     pools.create(poolName, 8);
     pools.edit_pool_pg(poolName, 4, false);
@@ -15,12 +16,14 @@ describe('Notification page', () => {
 
   after(() => {
     cy.login();
+    Cypress.Cookies.preserveOnce('token');
     pools.navigateTo();
     pools.delete(poolName);
   });
 
   beforeEach(() => {
     cy.login();
+    Cypress.Cookies.preserveOnce('token');
     pools.navigateTo();
   });
 
index 7e76f168e6df66046f439dc66e982bf47dd2ff08..c3f325dbbe13d19a0186964ad8bc98b3bbeeeffe 100644 (file)
@@ -6,6 +6,7 @@ describe('Role Management page', () => {
 
   beforeEach(() => {
     cy.login();
+    Cypress.Cookies.preserveOnce('token');
     roleMgmt.navigateTo();
   });
 
index 57818db0ae7547f1d3bf5bf6b8c363f5fc73ea94..92dc772121b1180927d8d299a7a2f94c86cc39f1 100644 (file)
@@ -6,6 +6,7 @@ describe('User Management page', () => {
 
   beforeEach(() => {
     cy.login();
+    Cypress.Cookies.preserveOnce('token');
     userMgmt.navigateTo();
   });
 
index 3d6dfeafa18573b28554921fc65a77c17f90de97..60fa7a746c043cba2f39e5f45040e09865c576a6 100644 (file)
@@ -13,7 +13,6 @@ let auth: any;
 
 const fillAuth = () => {
   window.localStorage.setItem('dashboard_username', auth.username);
-  window.localStorage.setItem('access_token', auth.token);
   window.localStorage.setItem('dashboard_permissions', auth.permissions);
   window.localStorage.setItem('user_pwd_expiration_date', auth.pwdExpirationDate);
   window.localStorage.setItem('user_pwd_update_required', auth.pwdUpdateRequired);
index b12e6fbcc0a1a29263cd8f83fd707923c866d479..d43b6b4ee61ccf78e5165bc90c668ff8e43d5152 100644 (file)
         "tslib": "^2.0.0"
       }
     },
-    "@auth0/angular-jwt": {
-      "version": "5.0.1",
-      "resolved": "https://registry.npmjs.org/@auth0/angular-jwt/-/angular-jwt-5.0.1.tgz",
-      "integrity": "sha512-djllMh6rthPscEj5n5T9zF223q8t+sDqnUuAYTJjdKoHvMAzYwwi2yP67HbojqjODG4ZLFAcPtRuzGgp+r7nDQ==",
-      "requires": {
-        "tslib": "^2.0.0"
-      }
-    },
     "@babel/code-frame": {
       "version": "7.10.4",
       "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz",
index ef75886fe296de1286302226b6bcd605100241a0..0bdbdfd1fa2b6459c569cb9421da024869774a63 100644 (file)
@@ -84,7 +84,6 @@
     "@angular/platform-browser": "10.1.5",
     "@angular/platform-browser-dynamic": "10.1.5",
     "@angular/router": "10.1.5",
-    "@auth0/angular-jwt": "5.0.1",
     "@circlon/angular-tree-component": "10.0.0",
     "@ng-bootstrap/ng-bootstrap": "7.0.0",
     "@swimlane/ngx-datatable": "18.0.0",
index dc8e81b39d4d89d1b68c70e1ded0b38b21418c15..2f59a0175ac730999db86feedd7beb744c947442 100644 (file)
@@ -3,7 +3,6 @@ import { ErrorHandler, NgModule } from '@angular/core';
 import { BrowserModule } from '@angular/platform-browser';
 import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
 
-import { JwtModule } from '@auth0/angular-jwt';
 import { ToastrModule } from 'ngx-toastr';
 
 import { AppRoutingModule } from './app-routing.module';
@@ -14,10 +13,6 @@ import { ApiInterceptorService } from './shared/services/api-interceptor.service
 import { JsErrorHandler } from './shared/services/js-error-handler.service';
 import { SharedModule } from './shared/shared.module';
 
-export function jwtTokenGetter() {
-  return localStorage.getItem('access_token');
-}
-
 @NgModule({
   declarations: [AppComponent],
   imports: [
@@ -32,12 +27,7 @@ export function jwtTokenGetter() {
     AppRoutingModule,
     CoreModule,
     SharedModule,
-    CephModule,
-    JwtModule.forRoot({
-      config: {
-        tokenGetter: jwtTokenGetter
-      }
-    })
+    CephModule
   ],
   exports: [SharedModule],
   providers: [
index 582eae7dc939739f1cd32e783eddf937f9705fc5..ca72007ee81e5b973d5b6dd852318b488d895dc0 100644 (file)
@@ -96,7 +96,7 @@ describe('RbdSnapshotListComponent', () => {
       rbdService = new RbdService(null, null);
       notificationService = new NotificationService(null, null, null);
       authStorageService = new AuthStorageService();
-      authStorageService.set('user', '', { 'rbd-image': ['create', 'read', 'update', 'delete'] });
+      authStorageService.set('user', { 'rbd-image': ['create', 'read', 'update', 'delete'] });
       component = new RbdSnapshotListComponent(
         authStorageService,
         modalService,
index 749fadd1a01eb68354cfe0b2aaa6bb4665d7bf6b..868ba66a002489cbd1e7ab3f48cfb2e8e46c7861 100644 (file)
@@ -51,7 +51,6 @@ export class LoginComponent implements OnInit {
         } else {
           this.authStorageService.set(
             login.username,
-            token,
             login.permissions,
             login.sso,
             login.pwdExpirationDate
index 6be293083123d8f6061a2b50a5c8a0a2c9567341..a76275bbec98c1499fa68b978b89b32979fe1fa3 100644 (file)
@@ -3,7 +3,6 @@ import { Component, OnInit, ViewChild } from '@angular/core';
 import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
 
 import { Icons } from '~/app/shared/enum/icons.enum';
-import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
 import { DocService } from '~/app/shared/services/doc.service';
 import { ModalService } from '~/app/shared/services/modal.service';
 import { AboutComponent } from '../about/about.component';
@@ -20,11 +19,7 @@ export class DashboardHelpComponent implements OnInit {
   modalRef: NgbModalRef;
   icons = Icons;
 
-  constructor(
-    private modalService: ModalService,
-    private authStorageService: AuthStorageService,
-    private docService: DocService
-  ) {}
+  constructor(private modalService: ModalService, private docService: DocService) {}
 
   ngOnInit() {
     this.docService.subscribeOnce('dashboard', (url: string) => {
@@ -37,8 +32,6 @@ export class DashboardHelpComponent implements OnInit {
   }
 
   goToApiDocs() {
-    const tokenInput = this.docsFormElement.nativeElement.children[0];
-    tokenInput.value = this.authStorageService.getToken();
     this.docsFormElement.nativeElement.submit();
   }
 }
index 5c759804d40e16dd662213121ec6885343e608b9..c32f0ea05fc9638ff060dbae35b4621ce386ebca 100644 (file)
@@ -33,7 +33,7 @@ describe('AuthService', () => {
 
   it('should login and save the user', fakeAsync(() => {
     const fakeCredentials = { username: 'foo', password: 'bar' };
-    const fakeResponse = { username: 'foo', token: 'tokenbytes' };
+    const fakeResponse = { username: 'foo' };
     service.login(fakeCredentials).subscribe();
     const req = httpTesting.expectOne('api/auth');
     expect(req.request.method).toBe('POST');
@@ -41,7 +41,6 @@ describe('AuthService', () => {
     req.flush(fakeResponse);
     tick();
     expect(localStorage.getItem('dashboard_username')).toBe('foo');
-    expect(localStorage.getItem('access_token')).toBe('tokenbytes');
   }));
 
   it('should logout and remove the user', () => {
index b8bf955832387ad13facb57e5f7427b030d44ea9..6c8356af87395727056768fe947e3ce0eda884bf 100644 (file)
@@ -28,7 +28,6 @@ export class AuthService {
       tap((resp: LoginResponse) => {
         this.authStorageService.set(
           resp.username,
-          resp.token,
           resp.permissions,
           resp.sso,
           resp.pwdExpirationDate,
@@ -40,8 +39,8 @@ export class AuthService {
 
   logout(callback: Function = null) {
     return this.http.post('api/auth/logout', null).subscribe((resp: any) => {
-      this.router.navigate(['/login'], { skipLocationChange: true });
       this.authStorageService.remove();
+      this.router.navigate(['/login'], { skipLocationChange: true });
       if (callback) {
         callback();
       }
index 7b9fc4b277e714f59509cf988b574a1c294b717f..12b4b8348ea2829a3a321980a10f6f7a74a165ae 100644 (file)
@@ -1,6 +1,5 @@
 export class LoginResponse {
   username: string;
-  token: string;
   permissions: object;
   pwdExpirationDate: number;
   sso: boolean;
index 67c093de6ec6e9fbb7533f122a43221bb9c06f48..f202c095f4791015181a2e2440b999b0dfe21088 100644 (file)
@@ -34,13 +34,13 @@ describe('AuthStorageService', () => {
   });
 
   it('should be SSO', () => {
-    service.set(username, '', {}, true);
+    service.set(username, {}, true);
     expect(localStorage.getItem('sso')).toBe('true');
     expect(service.isSSO()).toBe(true);
   });
 
   it('should not be SSO', () => {
-    service.set(username, '');
+    service.set(username);
     expect(localStorage.getItem('sso')).toBe('false');
     expect(service.isSSO()).toBe(false);
   });
index 5752f78c28afffaafe0f1296f3213a33834fdd85..15e21f9ed540dc7c8e5e307121eeba277b50b289 100644 (file)
@@ -13,14 +13,12 @@ export class AuthStorageService {
 
   set(
     username: string,
-    token: string,
     permissions = {},
     sso = false,
     pwdExpirationDate: number = null,
     pwdUpdateRequired: boolean = false
   ) {
     localStorage.setItem('dashboard_username', username);
-    localStorage.setItem('access_token', token);
     localStorage.setItem('dashboard_permissions', JSON.stringify(new Permissions(permissions)));
     localStorage.setItem('user_pwd_expiration_date', String(pwdExpirationDate));
     localStorage.setItem('user_pwd_update_required', String(pwdUpdateRequired));
@@ -28,16 +26,11 @@ export class AuthStorageService {
   }
 
   remove() {
-    localStorage.removeItem('access_token');
     localStorage.removeItem('dashboard_username');
     localStorage.removeItem('user_pwd_expiration_data');
     localStorage.removeItem('user_pwd_update_required');
   }
 
-  getToken(): string {
-    return localStorage.getItem('access_token');
-  }
-
   isLoggedIn() {
     return localStorage.getItem('dashboard_username') !== null;
   }
index 610484b0e076784907dd94cfe53962a0d2d546f7..c44963ca7ed147cd4fa2255856389310bff6ebec 100644 (file)
@@ -67,12 +67,20 @@ class JwtManager(object):
 
     @classmethod
     def get_token_from_header(cls):
-        auth_header = cherrypy.request.headers.get('authorization')
-        if auth_header is not None:
-            scheme, params = auth_header.split(' ', 1)
-            if scheme.lower() == 'bearer':
-                return params
-        return None
+        auth_cookie_name = 'token'
+        try:
+            # use cookie
+            return cherrypy.request.cookie[auth_cookie_name].value
+        except KeyError:
+            try:
+                # fall-back: use Authorization header
+                auth_header = cherrypy.request.headers.get('authorization')
+                if auth_header is not None:
+                    scheme, params = auth_header.split(' ', 1)
+                    if scheme.lower() == 'bearer':
+                        return params
+            except IndexError:
+                return None
 
     @classmethod
     def set_user(cls, username):