Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Port asymmetric algo JWT support to 3.3.x #11869

Merged
merged 2 commits into from
Dec 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .typo-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -193,5 +193,6 @@ excluded_words:
- Juste
- Tanja
- Vova
- conftest

spellcheck_filenames: false
2 changes: 2 additions & 0 deletions changelog/11869.bugfix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Implements a new CLI option `--jwt-private-key` required to have complete support for asymmetric algorithms as specified
originally in the docs.
12 changes: 12 additions & 0 deletions docs/docs/http-api.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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 <public_key> \
--jwt-private-key <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.
Expand Down
7 changes: 7 additions & 0 deletions rasa/cli/arguments/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
)
4 changes: 4 additions & 0 deletions rasa/core/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
)
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -185,6 +188,7 @@ def serve_application(
enable_api,
response_timeout,
jwt_secret,
jwt_private_key,
jwt_method,
port=port,
endpoints=endpoints,
Expand Down
4 changes: 3 additions & 1 deletion rasa/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
Expand All @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion tests/cli/test_rasa_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]"""
)

Expand Down
68 changes: 68 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import random
import textwrap

import jwt
import pytest
import sys
import uuid
Expand Down Expand Up @@ -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")
Expand Down
23 changes: 23 additions & 0 deletions tests/test_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)

Expand Down
3 changes: 3 additions & 0 deletions trivy-secret.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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