-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat!: Issue JWT for session pre-authentication
Instead of the non-structured session token, issue a JWT containing `session_id`, `user_id`, `user_name` and `user_role`. More claims will be added in the future. During session connection, the backend issues a signed JWT token. The private key is auto-generated in the backend (if it doesn't exist) and can be exchanged via new CLI endpoints. The JWT is validated automatically for all requests to HTTP-based sessions. The JWT can be read from the `ccm_session_token` cookie and can be trusted by sessions. It may be used to extract user or session information in the sessions. The validate_token endpoint doesn't require an active database session anymore, reducing network traffic and improving the response times. This effectively makes sessions faster and improved stability. BREAKING CHANGE: Users with active sessions have to reconnect to their sessions after the update has been rolled out. We recommend to install the update when there are no active sessions.
- Loading branch information
1 parent
5cb85b9
commit 99fe4b6
Showing
27 changed files
with
625 additions
and
94 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -75,6 +75,7 @@ repos: | |
- capellambse | ||
- typer | ||
- types-lxml | ||
- cryptography | ||
- repo: local | ||
hooks: | ||
- id: pylint | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors | ||
# SPDX-License-Identifier: Apache-2.0 | ||
|
||
from __future__ import annotations | ||
|
||
import enum | ||
import pathlib | ||
|
||
import typer | ||
|
||
from capellacollab.sessions import auth as sessions_auth | ||
|
||
app = typer.Typer( | ||
help="Import and export RSA keys used for session pre-authentication." | ||
) | ||
|
||
|
||
@app.command(name="import") | ||
def import_private_key(file: pathlib.Path): | ||
"""Read and load a private key from a file. | ||
After importing the key, it will be used to sign the JWT session tokens. | ||
The previous key will be discarded. | ||
Please note that we can only accept private keys which have | ||
been exported using the `export` command of this CLI. | ||
""" | ||
|
||
key = sessions_auth.load_private_key_from_disk(file) | ||
if key is None: | ||
raise typer.BadParameter( | ||
"The provided file does not contain a valid RSA private key." | ||
) | ||
|
||
sessions_auth.save_private_key_to_disk(key, sessions_auth.PRIVATE_KEY_PATH) | ||
sessions_auth.load_private_key_in_memory(key) | ||
|
||
|
||
class KeyType(str, enum.Enum): | ||
PRIVATE = "private" | ||
PUBLIC = "public" | ||
|
||
|
||
@app.command(name="export") | ||
def export_private_key( | ||
file: pathlib.Path, type: KeyType = typer.Option(default=KeyType.PRIVATE) | ||
): | ||
"""Export the current private or public key to a file. | ||
The private key will be exported in PEM format. | ||
""" | ||
|
||
private_key = sessions_auth.load_private_key_from_disk( | ||
sessions_auth.PRIVATE_KEY_PATH | ||
) | ||
if private_key is None: | ||
raise typer.BadParameter( | ||
"No private key has been loaded. Use the `import` command to load a key" | ||
" or start the backend once to auto-generate a key." | ||
) | ||
|
||
if type == KeyType.PRIVATE: | ||
sessions_auth.save_private_key_to_disk(private_key, file) | ||
else: | ||
with open(file, "wb") as f: | ||
f.write( | ||
private_key.public_key().public_bytes( | ||
encoding=sessions_auth.serialization.Encoding.PEM, | ||
format=sessions_auth.serialization.PublicFormat.SubjectPublicKeyInfo, | ||
) | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
# SPDX-FileCopyrightText: Copyright DB InfraGO AG and contributors | ||
# SPDX-License-Identifier: Apache-2.0 | ||
|
||
import logging | ||
import pathlib | ||
|
||
import appdirs | ||
from cryptography.hazmat.primitives import serialization | ||
from cryptography.hazmat.primitives.asymmetric import rsa | ||
|
||
PRIVATE_KEY: rsa.RSAPrivateKey | None = None | ||
PUBLIC_KEY: rsa.RSAPublicKey | None = None | ||
|
||
PRIVATE_KEY_PATH = ( | ||
pathlib.Path(appdirs.user_data_dir("capellacollab")) / "private_key.pem" | ||
) | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
|
||
def generate_private_key() -> rsa.RSAPrivateKey: | ||
logger.info( | ||
"Generating a new private key for session pre-authentication..." | ||
) | ||
return rsa.generate_private_key( | ||
public_exponent=65537, | ||
key_size=4096, | ||
) | ||
|
||
|
||
def serialize_private_key(key: rsa.RSAPrivateKey) -> bytes: | ||
return key.private_bytes( | ||
encoding=serialization.Encoding.PEM, | ||
format=serialization.PrivateFormat.PKCS8, | ||
encryption_algorithm=serialization.NoEncryption(), | ||
) | ||
|
||
|
||
def save_private_key_to_disk(key: rsa.RSAPrivateKey, path: pathlib.Path): | ||
logger.info( | ||
"Saving private key for session pre-authentication to %s", path | ||
) | ||
path.parent.mkdir(parents=True, exist_ok=True) | ||
with open(path, "wb") as f: | ||
f.write( | ||
serialize_private_key(key), | ||
) | ||
|
||
|
||
def load_private_key_from_disk(path: pathlib.Path) -> rsa.RSAPrivateKey | None: | ||
logger.info( | ||
"Trying to load private key for session pre-authentication from %s", | ||
path, | ||
) | ||
|
||
if not path.exists(): | ||
logger.info("No private key found at %s", path) | ||
return None | ||
|
||
with open(path, "rb") as f: | ||
key = serialization.load_pem_private_key( | ||
f.read(), | ||
password=None, | ||
) | ||
|
||
if not isinstance(key, rsa.RSAPrivateKey): | ||
logger.exception("The loaded private key is not an RSA key.") | ||
return None | ||
|
||
logger.info( | ||
"Successfully loaded private key for session pre-authentication from %s", | ||
path, | ||
) | ||
|
||
return key | ||
|
||
|
||
def load_private_key_in_memory(key: rsa.RSAPrivateKey): | ||
global PRIVATE_KEY | ||
global PUBLIC_KEY | ||
|
||
PRIVATE_KEY = key | ||
PUBLIC_KEY = PRIVATE_KEY.public_key() | ||
|
||
|
||
def initialize_session_pre_authentication(): | ||
private_key = load_private_key_from_disk(PRIVATE_KEY_PATH) | ||
|
||
if not private_key: | ||
private_key = generate_private_key() | ||
save_private_key_to_disk(private_key, PRIVATE_KEY_PATH) | ||
|
||
load_private_key_in_memory(private_key) | ||
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.