authorization: use ES256 algorithm for JWT signatures
Signed-off-by: Eric Callahan <arksine.code@gmail.com>
This commit is contained in:
parent
ce7f659a32
commit
8a3b885eca
|
@ -19,6 +19,13 @@ import logging
|
||||||
from jose import jwt
|
from jose import jwt
|
||||||
from tornado.ioloop import IOLoop, PeriodicCallback
|
from tornado.ioloop import IOLoop, PeriodicCallback
|
||||||
from tornado.web import HTTPError
|
from tornado.web import HTTPError
|
||||||
|
from cryptography.hazmat.primitives.asymmetric import ec
|
||||||
|
from cryptography.hazmat.primitives.serialization import (
|
||||||
|
PrivateFormat,
|
||||||
|
Encoding,
|
||||||
|
NoEncryption,
|
||||||
|
load_pem_private_key,
|
||||||
|
)
|
||||||
|
|
||||||
# Annotation imports
|
# Annotation imports
|
||||||
from typing import (
|
from typing import (
|
||||||
|
@ -30,6 +37,7 @@ from typing import (
|
||||||
Union,
|
Union,
|
||||||
Dict,
|
Dict,
|
||||||
List,
|
List,
|
||||||
|
cast,
|
||||||
)
|
)
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from confighelper import ConfigHelper
|
from confighelper import ConfigHelper
|
||||||
|
@ -42,6 +50,8 @@ if TYPE_CHECKING:
|
||||||
IPNetwork = Union[ipaddress.IPv4Network, ipaddress.IPv6Network]
|
IPNetwork = Union[ipaddress.IPv4Network, ipaddress.IPv6Network]
|
||||||
OneshotToken = Tuple[IPAddr, Optional[Dict[str, Any]], object]
|
OneshotToken = Tuple[IPAddr, Optional[Dict[str, Any]], object]
|
||||||
|
|
||||||
|
ECPrivateKey = ec.EllipticCurvePrivateKeyWithSerialization
|
||||||
|
|
||||||
ONESHOT_TIMEOUT = 5
|
ONESHOT_TIMEOUT = 5
|
||||||
TRUSTED_CONNECTION_TIMEOUT = 3600
|
TRUSTED_CONNECTION_TIMEOUT = 3600
|
||||||
PRUNE_CHECK_TIME = 300 * 1000
|
PRUNE_CHECK_TIME = 300 * 1000
|
||||||
|
@ -52,7 +62,7 @@ TRUSTED_USER = "_TRUSTED_USER_"
|
||||||
RESERVED_USERS = [API_USER, TRUSTED_USER]
|
RESERVED_USERS = [API_USER, TRUSTED_USER]
|
||||||
JWT_EXP_TIME = datetime.timedelta(hours=1)
|
JWT_EXP_TIME = datetime.timedelta(hours=1)
|
||||||
JWT_HEADER = {
|
JWT_HEADER = {
|
||||||
'alg': "HS256",
|
'alg': "ES256",
|
||||||
'typ': "JWT"
|
'typ': "JWT"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,11 +84,25 @@ class Authorization:
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
self.api_key = api_user['api_key']
|
self.api_key = api_user['api_key']
|
||||||
|
host_name, port = self.server.get_host_info()
|
||||||
|
self.issuer = f"http://{host_name}:{port}"
|
||||||
|
self.public_keys: Dict[str, ec.EllipticCurvePublicKey] = {}
|
||||||
|
for username, user_info in list(self.users.items()):
|
||||||
|
if username == API_USER:
|
||||||
|
continue
|
||||||
|
if 'jwt_secret' in user_info:
|
||||||
|
try:
|
||||||
|
priv_key = self._load_private_key(user_info['jwt_secret'])
|
||||||
|
except self.server.error:
|
||||||
|
logging.info("Invalid key found for user, removing")
|
||||||
|
user_info.pop('jwt_secret', None)
|
||||||
|
self.users[username] = user_info
|
||||||
|
continue
|
||||||
|
self.public_keys[username] = priv_key.public_key()
|
||||||
|
|
||||||
self.trusted_users: Dict[IPAddr, Any] = {}
|
self.trusted_users: Dict[IPAddr, Any] = {}
|
||||||
self.oneshot_tokens: Dict[str, OneshotToken] = {}
|
self.oneshot_tokens: Dict[str, OneshotToken] = {}
|
||||||
self.permitted_paths: Set[str] = set()
|
self.permitted_paths: Set[str] = set()
|
||||||
host_name, port = self.server.get_host_info()
|
|
||||||
self.issuer = f"http://{host_name}:{port}"
|
|
||||||
|
|
||||||
# Get allowed cors domains
|
# Get allowed cors domains
|
||||||
self.cors_domains: List[str] = []
|
self.cors_domains: List[str] = []
|
||||||
|
@ -189,6 +213,7 @@ class Authorization:
|
||||||
raise self.server.error(
|
raise self.server.error(
|
||||||
f"Invalid log out request for user {username}")
|
f"Invalid log out request for user {username}")
|
||||||
self.users.pop(f"{username}.jwt_secret", None)
|
self.users.pop(f"{username}.jwt_secret", None)
|
||||||
|
self.public_keys.pop(username, None)
|
||||||
return {
|
return {
|
||||||
"username": username,
|
"username": username,
|
||||||
"action": "user_logged_out"
|
"action": "user_logged_out"
|
||||||
|
@ -200,8 +225,11 @@ class Authorization:
|
||||||
refresh_token: str = web_request.get_str('refresh_token')
|
refresh_token: str = web_request.get_str('refresh_token')
|
||||||
user_info = self._decode_jwt(refresh_token, token_type="refresh")
|
user_info = self._decode_jwt(refresh_token, token_type="refresh")
|
||||||
username: str = user_info['username']
|
username: str = user_info['username']
|
||||||
secret = user_info['jwt_secret']
|
secret: Optional[str] = user_info.get('jwt_secret', None)
|
||||||
token = self._generate_jwt(username, secret)
|
if secret is None:
|
||||||
|
raise self.server.error("User not logged in", 401)
|
||||||
|
private_key = self._load_private_key(secret)
|
||||||
|
token = self._generate_jwt(username, private_key)
|
||||||
return {
|
return {
|
||||||
'username': username,
|
'username': username,
|
||||||
'token': token,
|
'token': token,
|
||||||
|
@ -306,14 +334,19 @@ class Authorization:
|
||||||
action = "user_logged_in"
|
action = "user_logged_in"
|
||||||
if hashed_pass != user_info['password']:
|
if hashed_pass != user_info['password']:
|
||||||
raise self.server.error("Invalid Password")
|
raise self.server.error("Invalid Password")
|
||||||
jwt_secret: Optional[str] = user_info.get('jwt_secret', None)
|
jwt_secret_hex: Optional[str] = user_info.get('jwt_secret', None)
|
||||||
if jwt_secret is None:
|
if jwt_secret_hex is None:
|
||||||
jwt_secret = secrets.token_bytes(32).hex()
|
private_key = ec.generate_private_key(ec.SECP256R1())
|
||||||
user_info['jwt_secret'] = jwt_secret
|
serialized: bytes = cast(ECPrivateKey, private_key).private_bytes(
|
||||||
|
Encoding.PEM, PrivateFormat.PKCS8, NoEncryption())
|
||||||
|
user_info['jwt_secret'] = serialized.hex()
|
||||||
self.users[username] = user_info
|
self.users[username] = user_info
|
||||||
token = self._generate_jwt(username, jwt_secret)
|
else:
|
||||||
|
private_key = self._load_private_key(jwt_secret_hex)
|
||||||
|
self.public_keys[username] = private_key.public_key()
|
||||||
|
token = self._generate_jwt(username, private_key)
|
||||||
refresh_token = self._generate_jwt(
|
refresh_token = self._generate_jwt(
|
||||||
username, jwt_secret, token_type="refresh",
|
username, private_key, token_type="refresh",
|
||||||
exp_time=datetime.timedelta(days=self.login_timeout))
|
exp_time=datetime.timedelta(days=self.login_timeout))
|
||||||
if create:
|
if create:
|
||||||
IOLoop.current().call_later(
|
IOLoop.current().call_later(
|
||||||
|
@ -342,6 +375,7 @@ class Authorization:
|
||||||
if user_info is None:
|
if user_info is None:
|
||||||
raise self.server.error(f"No registered user: {username}")
|
raise self.server.error(f"No registered user: {username}")
|
||||||
del self.users[username]
|
del self.users[username]
|
||||||
|
self.public_keys.pop(username, None)
|
||||||
IOLoop.current().call_later(
|
IOLoop.current().call_later(
|
||||||
.005, self.server.send_event,
|
.005, self.server.send_event,
|
||||||
"authorization:user_deleted",
|
"authorization:user_deleted",
|
||||||
|
@ -353,7 +387,7 @@ class Authorization:
|
||||||
|
|
||||||
def _generate_jwt(self,
|
def _generate_jwt(self,
|
||||||
username: str,
|
username: str,
|
||||||
secret: str,
|
secret: ec.EllipticCurvePrivateKey,
|
||||||
token_type: str = "access",
|
token_type: str = "access",
|
||||||
exp_time: datetime.timedelta = JWT_EXP_TIME
|
exp_time: datetime.timedelta = JWT_EXP_TIME
|
||||||
) -> str:
|
) -> str:
|
||||||
|
@ -366,7 +400,8 @@ class Authorization:
|
||||||
'username': username,
|
'username': username,
|
||||||
'token_type': token_type
|
'token_type': token_type
|
||||||
}
|
}
|
||||||
return jwt.encode(payload, secret, headers=JWT_HEADER)
|
return jwt.encode(payload, secret, algorithm="ES256",
|
||||||
|
headers=JWT_HEADER)
|
||||||
|
|
||||||
def _decode_jwt(self,
|
def _decode_jwt(self,
|
||||||
token: str,
|
token: str,
|
||||||
|
@ -386,14 +421,23 @@ class Authorization:
|
||||||
if user_info is None:
|
if user_info is None:
|
||||||
raise self.server.error(
|
raise self.server.error(
|
||||||
f"Invalid JWT, no registered user {username}", 401)
|
f"Invalid JWT, no registered user {username}", 401)
|
||||||
jwt_secret: Optional[str] = user_info.get('jwt_secret', None)
|
public_key = self.public_keys.get(username, None)
|
||||||
if jwt_secret is None:
|
if public_key is None:
|
||||||
raise self.server.error(
|
raise self.server.error(
|
||||||
f"Invalid JWT, user {username} not logged in", 401)
|
f"Invalid JWT, user {username} not logged in", 401)
|
||||||
jwt.decode(token, jwt_secret, algorithms=['HS256'],
|
jwt.decode(token, [public_key], algorithms=['ES256'],
|
||||||
audience="Moonraker")
|
audience="Moonraker")
|
||||||
return user_info
|
return user_info
|
||||||
|
|
||||||
|
def _load_private_key(self, secret: str) -> ec.EllipticCurvePrivateKey:
|
||||||
|
try:
|
||||||
|
key = load_pem_private_key(bytes.fromhex(secret), None)
|
||||||
|
except Exception:
|
||||||
|
raise self.server.error(
|
||||||
|
"Error decoding private key, user data may"
|
||||||
|
" be corrupt", 500) from None
|
||||||
|
return key
|
||||||
|
|
||||||
def _prune_conn_handler(self) -> None:
|
def _prune_conn_handler(self) -> None:
|
||||||
cur_time = time.time()
|
cur_time = time.time()
|
||||||
for ip, user_info in list(self.trusted_users.items()):
|
for ip, user_info in list(self.trusted_users.items()):
|
||||||
|
|
Loading…
Reference in New Issue