Skip to content

Commit c52c2dc

Browse files
authored
Merge pull request ceph#54710 from kalaspuffar/replace_jwt_with_pure_python
mgr/dashboard: Simplify authentication protocol Reviewed-by: Fabian-Gruenbichler <NOT@FOUND> Reviewed-by: Avan Thakkar <[email protected]> Reviewed-by: Ernesto Puerta <[email protected]> Reviewed-by: Nizamudeen A <[email protected]>
2 parents 33ee4fe + 06765e6 commit c52c2dc

File tree

8 files changed

+75
-16
lines changed

8 files changed

+75
-16
lines changed

ceph.spec.in

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -413,7 +413,6 @@ BuildRequires: xmlsec1-nss
413413
BuildRequires: xmlsec1-openssl
414414
BuildRequires: xmlsec1-openssl-devel
415415
BuildRequires: python%{python3_pkgversion}-cherrypy
416-
BuildRequires: python%{python3_pkgversion}-jwt
417416
BuildRequires: python%{python3_pkgversion}-routes
418417
BuildRequires: python%{python3_pkgversion}-scipy
419418
BuildRequires: python%{python3_pkgversion}-werkzeug
@@ -426,7 +425,6 @@ BuildRequires: libxmlsec1-1
426425
BuildRequires: libxmlsec1-nss1
427426
BuildRequires: libxmlsec1-openssl1
428427
BuildRequires: python%{python3_pkgversion}-CherryPy
429-
BuildRequires: python%{python3_pkgversion}-PyJWT
430428
BuildRequires: python%{python3_pkgversion}-Routes
431429
BuildRequires: python%{python3_pkgversion}-Werkzeug
432430
BuildRequires: python%{python3_pkgversion}-numpy-devel
@@ -628,7 +626,6 @@ Requires: ceph-prometheus-alerts = %{_epoch_prefix}%{version}-%{release}
628626
Requires: python%{python3_pkgversion}-setuptools
629627
%if 0%{?fedora} || 0%{?rhel} || 0%{?openEuler}
630628
Requires: python%{python3_pkgversion}-cherrypy
631-
Requires: python%{python3_pkgversion}-jwt
632629
Requires: python%{python3_pkgversion}-routes
633630
Requires: python%{python3_pkgversion}-werkzeug
634631
%if 0%{?weak_deps}
@@ -637,7 +634,6 @@ Recommends: python%{python3_pkgversion}-saml
637634
%endif
638635
%if 0%{?suse_version}
639636
Requires: python%{python3_pkgversion}-CherryPy
640-
Requires: python%{python3_pkgversion}-PyJWT
641637
Requires: python%{python3_pkgversion}-Routes
642638
Requires: python%{python3_pkgversion}-Werkzeug
643639
Recommends: python%{python3_pkgversion}-python3-saml

debian/control

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,6 @@ Build-Depends: automake,
9191
python3-all-dev,
9292
python3-cherrypy3,
9393
python3-natsort,
94-
python3-jwt <pkg.ceph.check>,
9594
python3-pecan <pkg.ceph.check>,
9695
python3-bcrypt <pkg.ceph.check>,
9796
tox <pkg.ceph.check>,

src/pybind/mgr/dashboard/constraints.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
CherryPy~=13.1
22
more-itertools~=8.14
3-
PyJWT~=2.0
43
bcrypt~=3.1
54
python3-saml~=1.4
65
requests~=2.26

src/pybind/mgr/dashboard/exceptions.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,3 +121,15 @@ class GrafanaError(Exception):
121121

122122
class PasswordPolicyException(Exception):
123123
pass
124+
125+
126+
class ExpiredSignatureError(Exception):
127+
pass
128+
129+
130+
class InvalidTokenError(Exception):
131+
pass
132+
133+
134+
class InvalidAlgorithmError(Exception):
135+
pass

src/pybind/mgr/dashboard/requirements-lint.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ autopep8==1.5.7
99
pyfakefs==4.5.0
1010
isort==5.5.3
1111
jsonschema~=4.0
12+
PyJWT~=2.0

src/pybind/mgr/dashboard/requirements-test.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ pytest-cov
22
pytest-instafail
33
pyfakefs==4.5.0
44
jsonschema~=4.0
5+
PyJWT~=2.0

src/pybind/mgr/dashboard/requirements.txt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
bcrypt
22
CherryPy
33
more-itertools
4-
PyJWT
54
pyopenssl
65
requests
76
Routes

src/pybind/mgr/dashboard/services/auth.py

Lines changed: 61 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
11
# -*- coding: utf-8 -*-
22

3+
import base64
4+
import hashlib
5+
import hmac
36
import json
47
import logging
58
import os
69
import threading
710
import time
811
import uuid
9-
from base64 import b64encode
1012

1113
import cherrypy
12-
import jwt
1314

1415
from .. import mgr
16+
from ..exceptions import ExpiredSignatureError, InvalidAlgorithmError, InvalidTokenError
1517
from .access_control import LocalAuthenticator, UserDoesNotExist
1618

1719
cherrypy.config.update({
@@ -33,7 +35,7 @@ class JwtManager(object):
3335
@staticmethod
3436
def _gen_secret():
3537
secret = os.urandom(16)
36-
return b64encode(secret).decode('utf-8')
38+
return base64.b64encode(secret).decode('utf-8')
3739

3840
@classmethod
3941
def init(cls):
@@ -45,6 +47,54 @@ def init(cls):
4547
mgr.set_store('jwt_secret', secret)
4648
cls._secret = secret
4749

50+
@classmethod
51+
def array_to_base64_string(cls, message):
52+
jsonstr = json.dumps(message, sort_keys=True).replace(" ", "")
53+
string_bytes = base64.urlsafe_b64encode(bytes(jsonstr, 'UTF-8'))
54+
return string_bytes.decode('UTF-8').replace("=", "")
55+
56+
@classmethod
57+
def encode(cls, message, secret):
58+
header = {"alg": cls.JWT_ALGORITHM, "typ": "JWT"}
59+
base64_header = cls.array_to_base64_string(header)
60+
base64_message = cls.array_to_base64_string(message)
61+
base64_secret = base64.urlsafe_b64encode(hmac.new(
62+
bytes(secret, 'UTF-8'),
63+
msg=bytes(base64_header + "." + base64_message, 'UTF-8'),
64+
digestmod=hashlib.sha256
65+
).digest()).decode('UTF-8').replace("=", "")
66+
return base64_header + "." + base64_message + "." + base64_secret
67+
68+
@classmethod
69+
def decode(cls, message, secret):
70+
split_message = message.split(".")
71+
base64_header = split_message[0]
72+
base64_message = split_message[1]
73+
base64_secret = split_message[2]
74+
75+
decoded_header = json.loads(base64.urlsafe_b64decode(base64_header))
76+
77+
if decoded_header['alg'] != cls.JWT_ALGORITHM:
78+
raise InvalidAlgorithmError()
79+
80+
incoming_secret = base64.urlsafe_b64encode(hmac.new(
81+
bytes(secret, 'UTF-8'),
82+
msg=bytes(base64_header + "." + base64_message, 'UTF-8'),
83+
digestmod=hashlib.sha256
84+
).digest()).decode('UTF-8').replace("=", "")
85+
86+
if base64_secret != incoming_secret:
87+
raise InvalidTokenError()
88+
89+
# We add ==== as padding to ignore the requirement to have correct padding in
90+
# the urlsafe_b64decode method.
91+
decoded_message = json.loads(base64.urlsafe_b64decode(base64_message + "===="))
92+
now = int(time.time())
93+
if decoded_message['exp'] < now:
94+
raise ExpiredSignatureError()
95+
96+
return decoded_message
97+
4898
@classmethod
4999
def gen_token(cls, username):
50100
if not cls._secret:
@@ -59,13 +109,13 @@ def gen_token(cls, username):
59109
'iat': now,
60110
'username': username
61111
}
62-
return jwt.encode(payload, cls._secret, algorithm=cls.JWT_ALGORITHM) # type: ignore
112+
return cls.encode(payload, cls._secret) # type: ignore
63113

64114
@classmethod
65115
def decode_token(cls, token):
66116
if not cls._secret:
67117
cls.init()
68-
return jwt.decode(token, cls._secret, algorithms=cls.JWT_ALGORITHM) # type: ignore
118+
return cls.decode(token, cls._secret) # type: ignore
69119

70120
@classmethod
71121
def get_token_from_header(cls):
@@ -99,8 +149,8 @@ def get_username(cls):
99149
@classmethod
100150
def get_user(cls, token):
101151
try:
102-
dtoken = JwtManager.decode_token(token)
103-
if not JwtManager.is_blocklisted(dtoken['jti']):
152+
dtoken = cls.decode_token(token)
153+
if not cls.is_blocklisted(dtoken['jti']):
104154
user = AuthManager.get_user(dtoken['username'])
105155
if user.last_update <= dtoken['iat']:
106156
return user
@@ -110,10 +160,12 @@ def get_user(cls, token):
110160
)
111161
else:
112162
cls.logger.debug('Token is block-listed') # type: ignore
113-
except jwt.ExpiredSignatureError:
163+
except ExpiredSignatureError:
114164
cls.logger.debug("Token has expired") # type: ignore
115-
except jwt.InvalidTokenError:
165+
except InvalidTokenError:
116166
cls.logger.debug("Failed to decode token") # type: ignore
167+
except InvalidAlgorithmError:
168+
cls.logger.debug("Only the HS256 algorithm is supported.") # type: ignore
117169
except UserDoesNotExist:
118170
cls.logger.debug( # type: ignore
119171
"Invalid token: user %s does not exist", dtoken['username']

0 commit comments

Comments
 (0)