]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Use secure cookies to store JWT Token 39120/head
authorAashish Sharma <aashishsharma@localhost.localdomain>
Tue, 24 Nov 2020 05:58:28 +0000 (11:28 +0530)
committerAvan Thakkar <athakkar@redhat.com>
Fri, 29 Jan 2021 07:52:12 +0000 (13:22 +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)
(cherry picked from commit 3c72dc309936b23e413dc1aee8ca49c795c48a0f)

 Conflicts:
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/saml2.py
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/ui/language.e2e-spec.ts
src/pybind/mgr/dashboard/frontend/cypress/integration/ui/navigation.e2e-spec.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/core/navigation/dashboard-help/dashboard-help.component.ts
     - Adopting the changes from the master branch, ignoring few e2e changes
       as few files doesn't exist in octopus.

38 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/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/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 1a7a6951c97bec73f3d3aee28ea26589b79a0efb..64cbba9e3f623a69986a276651a91c1c81b574f2 100644 (file)
@@ -96,18 +96,19 @@ class DashboardTestCase(MgrTestCase):
             cls._ceph_cmd(set_roles_args)
 
     @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
@@ -195,29 +196,49 @@ class DashboardTestCase(MgrTestCase):
     def tearDownClass(cls):
         super(DashboardTestCase, cls).tearDownClass()
 
-    # pylint: disable=inconsistent-return-statements
+    # pylint: disable=inconsistent-return-statements, too-many-branches
     @classmethod
-    def _request(cls, url, method, data=None, params=None):
+    def _request(cls, url, method, data=None, params=None, 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 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:
+                cookies['token'] = cls._token
+            else:
+                headers['Authorization'] = "Bearer {}".format(cls._token)
+
+        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.
@@ -231,8 +252,8 @@ class DashboardTestCase(MgrTestCase):
             raise ex
 
     @classmethod
-    def _get(cls, url, params=None):
-        return cls._request(url, 'GET', params=params)
+    def _get(cls, url, params=None, set_cookies=False):
+        return cls._request(url, 'GET', params=params, set_cookies=set_cookies)
 
     @classmethod
     def _view_cache_get(cls, url, retries=5):
@@ -253,16 +274,16 @@ class DashboardTestCase(MgrTestCase):
         return res
 
     @classmethod
-    def _post(cls, url, data=None, params=None):
-        cls._request(url, 'POST', data, params)
+    def _post(cls, url, data=None, params=None, set_cookies=False):
+        cls._request(url, 'POST', data, params, set_cookies=set_cookies)
 
     @classmethod
-    def _delete(cls, url, data=None, params=None):
-        cls._request(url, 'DELETE', data, params)
+    def _delete(cls, url, data=None, params=None, set_cookies=False):
+        cls._request(url, 'DELETE', data, params, set_cookies=set_cookies)
 
     @classmethod
-    def _put(cls, url, data=None, params=None):
-        cls._request(url, 'PUT', data, params)
+    def _put(cls, url, data=None, params=None, set_cookies=False):
+        cls._request(url, 'PUT', data, params, set_cookies=set_cookies)
 
     @classmethod
     def _assertEq(cls, v1, v2):
@@ -281,8 +302,8 @@ class DashboardTestCase(MgrTestCase):
 
     # pylint: disable=too-many-arguments
     @classmethod
-    def _task_request(cls, method, url, data, timeout):
-        res = cls._request(url, method, data)
+    def _task_request(cls, method, url, data, timeout, set_cookies=False):
+        res = cls._request(url, method, data, set_cookies=set_cookies)
         cls._assertIn(cls._resp.status_code, [200, 201, 202, 204, 400, 403, 404])
 
         if cls._resp.status_code == 403:
@@ -334,16 +355,16 @@ class DashboardTestCase(MgrTestCase):
             return res_task['exception']
 
     @classmethod
-    def _task_post(cls, url, data=None, timeout=60):
-        return cls._task_request('POST', url, data, timeout)
+    def _task_post(cls, url, data=None, timeout=60, set_cookies=False):
+        return cls._task_request('POST', url, data, timeout, set_cookies=set_cookies)
 
     @classmethod
-    def _task_delete(cls, url, timeout=60):
-        return cls._task_request('DELETE', url, None, timeout)
+    def _task_delete(cls, url, timeout=60, set_cookies=False):
+        return cls._task_request('DELETE', url, None, timeout, set_cookies=set_cookies)
 
     @classmethod
-    def _task_put(cls, url, data=None, timeout=60):
-        return cls._task_request('PUT', url, data, timeout)
+    def _task_put(cls, url, data=None, timeout=60, set_cookies=False):
+        return cls._task_request('PUT', url, data, timeout, set_cookies=set_cookies)
 
     @classmethod
     def cookies(cls):
index 473a5a448fc1d404613f2b85370ed3b6f49d3fd8..e1c9b8e63e62eb42aabd4271fb0784ff964d25bf 100644 (file)
@@ -30,6 +30,7 @@ class AuthTest(DashboardTestCase):
             self.assertIn('delete', perms)
 
     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)
@@ -37,7 +38,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()
@@ -51,7 +61,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({
@@ -60,7 +85,17 @@ class AuthTest(DashboardTestCase):
             "detail": "Invalid credentials"
         })
 
+        # test with Cookies set
+        self._post("/api/auth", {'username': 'admin', 'password': 'inval'}, set_cookies=True)
+        self.assertStatus(400)
+        self.assertJsonBody({
+            "component": "auth",
+            "code": "invalid_credentials",
+            "detail": "Invalid credentials"
+        })
+
     def test_login_without_password(self):
+        # test with Authorization header
         self.create_user('admin2', '', ['administrator'])
         self._post("/api/auth", {'username': 'admin2', 'password': ''})
         self.assertStatus(400)
@@ -71,7 +106,19 @@ class AuthTest(DashboardTestCase):
         })
         self.delete_user('admin2')
 
+        # test with Cookies set
+        self.create_user('admin2', '', ['administrator'])
+        self._post("/api/auth", {'username': 'admin2', 'password': ''}, set_cookies=True)
+        self.assertStatus(400)
+        self.assertJsonBody({
+            "component": "auth",
+            "code": "invalid_credentials",
+            "detail": "Invalid credentials"
+        })
+        self.delete_user('admin2')
+
     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'})
@@ -96,7 +143,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()
@@ -111,7 +184,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)
@@ -124,7 +213,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_blacklist(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)
@@ -144,11 +247,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'])
@@ -172,7 +301,32 @@ 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(['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)
@@ -185,7 +339,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)
@@ -194,3 +362,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 d872b8a30144e66c005f87e7d1cddcd1b442ebf2..99fa37f258f11f2dd0c38e6c80458a08ace99b9c 100644 (file)
@@ -964,3 +964,12 @@ def allow_empty_body(func):  # noqa: N802
     except (AttributeError, KeyError):
         func._cp_config = {'tools.json_in.force': False}
     return func
+
+
+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 a66356c87c15e05a4e9604298e7c49efcc888285..d6dd12d6bda522eb8d00c56b888972269f4510ac 100644 (file)
@@ -1,16 +1,21 @@
 # -*- coding: utf-8 -*-
 from __future__ import absolute_import
 
+import http.cookies
 import logging
-import cherrypy
+import sys
 
 from . import ApiController, RESTController, \
-    allow_empty_body
+    allow_empty_body, set_cookies
 from .. import mgr
 from ..exceptions import InvalidCredentialsError, UserDoesNotExist
 from ..services.auth import AuthManager, JwtManager
 from ..settings import Settings
 
+# 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')
 
@@ -31,12 +36,13 @@ 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 09b646bc050726c12e69aabc73634966a6ef5cf5..125ff06616cccc670e648a5f923b07317a5a6cb4 100644 (file)
@@ -379,8 +379,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 4bafc6933ac99e919ed3a5c0c0ddeddcc6181273..810455e36abc708d957d67cc1b955e06d44a3702 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 Controller, Endpoint, BaseController
+from . import BaseController, Controller, Endpoint, allow_empty_body, set_cookies
 
 
 @Controller('/auth/saml2', secure=False)
@@ -43,6 +43,7 @@ class Saml2(BaseController):
             raise cherrypy.HTTPError(400, 'Single Sign-On is not configured.')
 
     @Endpoint('POST', path="")
+    @allow_empty_body
     def auth_response(self, **kwargs):
         Saml2._check_python_saml()
         req = Saml2._build_req(self._request, kwargs)
@@ -70,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 {
@@ -103,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 f7154fb59e02949969f7a75efdb5b0f3210e2fea..fdc2f637058dc03941ebe078bfdebd79eabafb44 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 ab55fee23599c2d8398ed9b14df3dabb19ff0588..dad4701da25743a1478a3406b7e45cebb6876d71 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 67d27011102ff2d2989f597262d9ef4bf94ffec7..0cbedfcfa1c711d890c64178c0ba4171933ed316 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 3503471a0a4b7a3cc48f0096768d9c9f0f3af922..1fb76e4d019b3650e7a4286bfe4a49c8e72fe7aa 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 fd2217579a52423c8c7b785f5d096bad30bbcfcb..5339b26be31764b96b8789cf0f5a7006a141699b 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 397745f9745d060253df88edad364d43c4021d45..2d604db34394e5dad389212502ff8817fe20b06c 100644 (file)
@@ -18,6 +18,7 @@ describe('Dashboard Main Page', () => {
 
   beforeEach(() => {
     cy.login();
+    Cypress.Cookies.preserveOnce('token');
     dashboard.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 0bcfe76ad12000f5132842bbd63620516f0cc3fb..57a06bf4e743924c69a01568061e6f50395644a3 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 4e6d4f9da993802aa30005e3e7e0c6677d824fbb..1a34514638dbb75fbc469b087b6e3e210cb263bf 100644 (file)
         "tslib": "^1.9.0"
       }
     },
-    "@auth0/angular-jwt": {
-      "version": "2.1.1",
-      "resolved": "https://registry.npmjs.org/@auth0/angular-jwt/-/angular-jwt-2.1.1.tgz",
-      "integrity": "sha512-kgTzPafHzoEQp5T9+FQMIaFfnacPwnzlvlwhCy6OYt/2w8uC8nFrN2pNQu29YnlJxLVXI5i0jgAT57+LrWFU/w==",
-      "requires": {
-        "url": "^0.11.0"
-      }
-    },
     "@babel/code-frame": {
       "version": "7.10.4",
       "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz",
index 75c9949270c1540de33d5f839f91f642cf167be3..43d832c14f895862796a9c1f01f9b752aaa09a5d 100644 (file)
@@ -85,7 +85,6 @@
     "@angular/platform-browser": "8.2.14",
     "@angular/platform-browser-dynamic": "8.2.14",
     "@angular/router": "8.2.14",
-    "@auth0/angular-jwt": "2.1.1",
     "@ngx-translate/i18n-polyfill": "1.0.0",
     "@swimlane/ngx-datatable": "16.0.3",
     "@types/file-saver": "^2.0.1",
index c66974e112b6667c6f1f20362bb0e1f66d146c2d..1f8e9bf5b16209d34d9157fd0f90afc118dbfca1 100644 (file)
@@ -9,7 +9,6 @@ import {
 import { BrowserModule } from '@angular/platform-browser';
 import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
 
-import { JwtModule } from '@auth0/angular-jwt';
 import { I18n } from '@ngx-translate/i18n-polyfill';
 import { NgBootstrapFormValidationModule } from 'ng-bootstrap-form-validation';
 
@@ -28,10 +27,6 @@ import { SharedModule } from './shared/shared.module';
 
 import { environment } from '../environments/environment';
 
-export function jwtTokenGetter() {
-  return localStorage.getItem('access_token');
-}
-
 @NgModule({
   declarations: [AppComponent],
   imports: [
@@ -50,11 +45,6 @@ export function jwtTokenGetter() {
     AccordionModule.forRoot(),
     BsDropdownModule.forRoot(),
     TabsModule.forRoot(),
-    JwtModule.forRoot({
-      config: {
-        tokenGetter: jwtTokenGetter
-      }
-    }),
     NgBootstrapFormValidationModule.forRoot()
   ],
   exports: [SharedModule],
index c032d0d522b52ccde27e6c58b737472796db1ec3..044e3568f04efb0d0dd3864e11cdd49448e661f4 100644 (file)
@@ -93,7 +93,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,
         null,
index 770a4821db177d62bf0d164126d33821916df540..47884af9c9feffa9b7ba3666551baab4dd03dfd2 100644 (file)
@@ -54,7 +54,6 @@ export class LoginComponent implements OnInit {
         } else {
           this.authStorageService.set(
             login.username,
-            token,
             login.permissions,
             login.sso,
             login.pwdExpirationDate
index 5d212d6cac43b1ca1f20c5615876db635a9db0bd..1cdaf523ba5851d9d0522d63e0b1ceb0d6f6f68b 100644 (file)
@@ -3,7 +3,6 @@ import { Component, OnInit, ViewChild } from '@angular/core';
 import { BsModalRef, BsModalService } from 'ngx-bootstrap/modal';
 
 import { Icons } from '../../../shared/enum/icons.enum';
-import { AuthStorageService } from '../../../shared/services/auth-storage.service';
 import { DocService } from '../../../shared/services/doc.service';
 import { AboutComponent } from '../about/about.component';
 
@@ -19,11 +18,7 @@ export class DashboardHelpComponent implements OnInit {
   modalRef: BsModalRef;
   icons = Icons;
 
-  constructor(
-    private modalService: BsModalService,
-    private authStorageService: AuthStorageService,
-    private docService: DocService
-  ) {}
+  constructor(private modalService: BsModalService, 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 1dffaf04a77b81de9fd6e8df5933294ef3ea0348..58e44a0e4cb49ea857b822ff75490a8cbc2778b2 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', fakeAsync(() => {
index c8ac216f78b91ce36af631296c201df6cdfbe44d..022f88f1800a0dafc770a9aeb272711b8432dfc8 100644 (file)
@@ -29,7 +29,6 @@ export class AuthService {
       tap((resp: LoginResponse) => {
         this.authStorageService.set(
           resp.username,
-          resp.token,
           resp.permissions,
           resp.sso,
           resp.pwdExpirationDate,
@@ -41,8 +40,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 fcc24cf5b957eda0196377622299ee4060b86357..97a56e65c281b7d09911b879fd110385f54266d1 100644 (file)
@@ -15,14 +15,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));
@@ -30,16 +28,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 d829362e63d3c9e2312dd54b6d8021fc1f23cac2..bbb8a2ecfe11c664663f3943982d383691c0c456 100644 (file)
@@ -63,12 +63,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):