diff --git a/.typo-ci.yml b/.typo-ci.yml index 3b371da4df65..f7153430359a 100644 --- a/.typo-ci.yml +++ b/.typo-ci.yml @@ -193,5 +193,6 @@ excluded_words: - Juste - Tanja - Vova + - conftest spellcheck_filenames: false diff --git a/docs/docs/http-api.mdx b/docs/docs/http-api.mdx index e1758d49c5eb..794e66bd2d30 100644 --- a/docs/docs/http-api.mdx +++ b/docs/docs/http-api.mdx @@ -85,6 +85,18 @@ rasa run \ --jwt-secret thisismysecret ``` +If you want to sign a JWT token with asymmetric algorithms, you can specify the JWT private key to the `--jwt-private-key` +CLI argument. You must pass the public key to the `--jwt-secret` argument, and also specify the algorithm to the +`--jwt-method` argument: + +```bash +rasa run \ + --enable-api \ + --jwt-secret \ + --jwt-private-key \ + --jwt-method RS512 +``` + Client requests to the server will need to contain a valid JWT token in the `Authorization` header that is signed using this secret and the `HS256` algorithm e.g. diff --git a/rasa/cli/arguments/run.py b/rasa/cli/arguments/run.py index 4e584a0e69c0..f982672700d1 100644 --- a/rasa/cli/arguments/run.py +++ b/rasa/cli/arguments/run.py @@ -166,3 +166,10 @@ def add_server_arguments(parser: argparse.ArgumentParser) -> None: default="HS256", help="Method used for the signature of the JWT authentication payload.", ) + jwt_auth.add_argument( + "--jwt-private-key", + type=str, + help="A private key used for generating web tokens, dependent upon " + "which hashing algorithm is used. It must be used together with " + "--jwt-secret for providing the public key.", + ) diff --git a/rasa/core/run.py b/rasa/core/run.py index 2172d15c6fe2..63e96c8721b4 100644 --- a/rasa/core/run.py +++ b/rasa/core/run.py @@ -83,6 +83,7 @@ def configure_app( enable_api: bool = True, response_timeout: int = constants.DEFAULT_RESPONSE_TIMEOUT, jwt_secret: Optional[Text] = None, + jwt_private_key: Optional[Text] = None, jwt_method: Optional[Text] = None, route: Optional[Text] = "/webhooks/", port: int = constants.DEFAULT_SERVER_PORT, @@ -106,6 +107,7 @@ def configure_app( auth_token=auth_token, response_timeout=response_timeout, jwt_secret=jwt_secret, + jwt_private_key=jwt_private_key, jwt_method=jwt_method, endpoints=endpoints, ) @@ -157,6 +159,7 @@ def serve_application( enable_api: bool = True, response_timeout: int = constants.DEFAULT_RESPONSE_TIMEOUT, jwt_secret: Optional[Text] = None, + jwt_private_key: Optional[Text] = None, jwt_method: Optional[Text] = None, endpoints: Optional[AvailableEndpoints] = None, remote_storage: Optional[Text] = None, @@ -185,6 +188,7 @@ def serve_application( enable_api, response_timeout, jwt_secret, + jwt_private_key, jwt_method, port=port, endpoints=endpoints, diff --git a/rasa/server.py b/rasa/server.py index 321d1dd63e5b..ae5f201b7d0c 100644 --- a/rasa/server.py +++ b/rasa/server.py @@ -645,6 +645,7 @@ def create_app( auth_token: Optional[Text] = None, response_timeout: int = DEFAULT_RESPONSE_TIMEOUT, jwt_secret: Optional[Text] = None, + jwt_private_key: Optional[Text] = None, jwt_method: Text = "HS256", endpoints: Optional[AvailableEndpoints] = None, ) -> Sanic: @@ -653,7 +654,7 @@ def create_app( app.config.RESPONSE_TIMEOUT = response_timeout configure_cors(app, cors_origins) - # Setup the Sanic-JWT extension + # Set up the Sanic-JWT extension if jwt_secret and jwt_method: # `sanic-jwt` depends on having an available event loop when making the call to # `Initialize`. If there is none, the server startup will fail with @@ -671,6 +672,7 @@ def create_app( Initialize( app, secret=jwt_secret, + private_key=jwt_private_key, authenticate=authenticate, algorithm=jwt_method, user_id="username", diff --git a/tests/cli/test_rasa_run.py b/tests/cli/test_rasa_run.py index cf8a9adcf805..eb53160d5149 100644 --- a/tests/cli/test_rasa_run.py +++ b/tests/cli/test_rasa_run.py @@ -46,7 +46,7 @@ def test_run_help( [--ssl-keyfile SSL_KEYFILE] [--ssl-ca-file SSL_CA_FILE] [--ssl-password SSL_PASSWORD] [--credentials CREDENTIALS] [--connector CONNECTOR] [--jwt-secret JWT_SECRET] - [--jwt-method JWT_METHOD] + [--jwt-method JWT_METHOD] [--jwt-private-key JWT_PRIVATE_KEY] {actions} ... [model-as-positional-argument]""" ) diff --git a/tests/conftest.py b/tests/conftest.py index 8200866453e4..fcc3d57ca62c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,6 +5,7 @@ import random import textwrap +import jwt import pytest import sys import uuid @@ -489,6 +490,73 @@ def rasa_server_secured(default_agent: Agent) -> Sanic: return app +@pytest.fixture +def test_public_key() -> Text: + test_public_key = """-----BEGIN PUBLIC KEY----- +MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC34ht9inqGq79HecpyOAnu2Cgv +jvgcpFifpFLPmCNdiomAgE48tfUAXJRoOGlVtrqc8KgQWjTFLjqDjUh1sBFF69Fl +wQGt7pgH10ZbERWpMTAbpjI9EoH74gDcmZ6Fy1VgQPbAwty3liw5Q5zqZLj7JhuX +Sa0EqvZQP+Hnayab7QIDAQAB +-----END PUBLIC KEY-----""" + + return test_public_key + + +@pytest.fixture +def test_private_key() -> Text: + test_private_key = """-----BEGIN RSA PRIVATE KEY----- +MIICXQIBAAKBgQC34ht9inqGq79HecpyOAnu2CgvjvgcpFifpFLPmCNdiomAgE48 +tfUAXJRoOGlVtrqc8KgQWjTFLjqDjUh1sBFF69FlwQGt7pgH10ZbERWpMTAbpjI9 +EoH74gDcmZ6Fy1VgQPbAwty3liw5Q5zqZLj7JhuXSa0EqvZQP+Hnayab7QIDAQAB +AoGBAIfUE25mjh9QWljX0/0O+/db4ENRHmE53OT/otQJk4YTQYKURDaASdvchxt9 +IAHamno3Ik4B9Bz7CuoFwNJ+HiMBf32KwJ75n/NZL17lBKst71z3r0gYCz6jcJxv +brbNs8qsLFyRMQz6NvS4d4GnXpGhc54IoJqtr/vR+Q87UwtZAkEA3AG78E7Fd5zT +sU/BO9E0VisQOysGcwPd9+rQPSyF8ncvaiMJ7STNvVsgrtJuw4DJq2RsMSJ77QgS +Ku6BJxB58wJBANX3dOEiNEZLJR+4LdNYRoR4gx2LcJW5PthwLi8ZOHBZeh9q3f2i +r5X5iPJ5kBRqajtYm634f/j8P4fxSdWzKp8CQQCNimQR92udR3z+HxRvWml0YmIf +3s9YYY2FeUEdii5mznznqMEzGzFt+Fmvf1yZVJrqNEJS3h+iYEXn7ueSbUw3AkBm +xSK4d+tP0AwWvioUlxPX0OJ5MF51K7LJ1qf4K072d6O2r2fMyXU4vdBPVqAjjjFU +K+0qlG8zMkV5kCV8pT/VAkA8bM5KRa73JY0bfGX4i8UZMFHzIq2KGjHlRES4vd+L +h18+hpcBAAyUR/jDT8nnG5YaYFz8rf2DnOy+elmmaYVm +-----END RSA PRIVATE KEY-----""" + + return test_private_key + + +@pytest.fixture +def asymmetric_jwt_method() -> Text: + return "RS256" + + +@pytest.fixture +def rasa_server_secured_asymmetric( + default_agent: Agent, + test_public_key: Text, + test_private_key: Text, + asymmetric_jwt_method: Text, +) -> Sanic: + app = server.create_app( + agent=default_agent, + auth_token="rasa", + jwt_secret=test_public_key, + jwt_private_key=test_private_key, + jwt_method=asymmetric_jwt_method, + ) + channel.register([RestInput()], app, "/webhooks/") + return app + + +@pytest.fixture +def encoded_jwt(test_private_key: Text, asymmetric_jwt_method: Text) -> Text: + payload = {"user": {"username": "myuser", "role": "admin"}} + encoded_jwt = jwt.encode( + payload=payload, + key=test_private_key, + algorithm=asymmetric_jwt_method, + ) + return encoded_jwt + + @pytest.fixture def rasa_non_trained_server_secured(empty_agent: Agent) -> Sanic: app = server.create_app(agent=empty_agent, auth_token="rasa", jwt_secret="core") diff --git a/tests/test_server.py b/tests/test_server.py index 62a738c34203..d1eeb1d03985 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -132,6 +132,13 @@ def rasa_secured_app(rasa_server_secured: Sanic) -> SanicASGITestClient: return rasa_server_secured.asgi_client +@pytest.fixture +def rasa_secured_app_asymmetric( + rasa_server_secured_asymmetric: Sanic, +) -> SanicASGITestClient: + return rasa_server_secured_asymmetric.asgi_client + + @pytest.fixture def rasa_non_trained_secured_app( rasa_non_trained_server_secured: Sanic, @@ -1374,6 +1381,22 @@ async def test_get_tracker_with_jwt(rasa_secured_app: SanicASGITestClient): assert response.status == HTTPStatus.OK +async def test_get_tracker_with_asymmetric_jwt( + rasa_secured_app_asymmetric: SanicASGITestClient, + encoded_jwt: Text, +) -> None: + jwt_header = {"Authorization": f"Bearer {encoded_jwt}"} + _, response = await rasa_secured_app_asymmetric.get( + "/conversations/myuser/tracker", headers=jwt_header + ) + assert response.status == HTTPStatus.OK + + _, response = await rasa_secured_app_asymmetric.get( + "/conversations/testuser/tracker", headers=jwt_header + ) + assert response.status == HTTPStatus.OK + + def test_list_routes(empty_agent: Agent): app = rasa.server.create_app(empty_agent, auth_token=None) diff --git a/trivy-secret.yaml b/trivy-secret.yaml index fd494d908477..fb8cf01d9ff7 100644 --- a/trivy-secret.yaml +++ b/trivy-secret.yaml @@ -2,3 +2,6 @@ allow-rules: - id: docs/docs/deploy/deploy-rasa.mdx description: Example service account in docs path: docs/docs/deploy/deploy-rasa.mdx + - id: tests/conftest.py + description: JWT private key used in unit testing + path: tests/conftest.py