feat(embedded): aud claim and type for guest token (#18651)

* add aud claim and type for guest token

* update test

* lint

* make jwt audience configurable

* lint

* Apply suggestions from code review

Co-authored-by: David Aaron Suddjian <1858430+suddjian@users.noreply.github.com>

* verify aud

* add tests for aud and type claim

Co-authored-by: David Aaron Suddjian <1858430+suddjian@users.noreply.github.com>
This commit is contained in:
Lily Kuang 2022-02-14 10:43:35 -08:00 committed by GitHub
parent 4001165f55
commit e6ea197e9f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 72 additions and 3 deletions

View File

@ -1316,6 +1316,7 @@ GUEST_TOKEN_JWT_SECRET = "test-guest-secret-change-me"
GUEST_TOKEN_JWT_ALGO = "HS256"
GUEST_TOKEN_HEADER_NAME = "X-GuestToken"
GUEST_TOKEN_JWT_EXP_SECONDS = 300 # 5 minutes
GUEST_TOKEN_JWT_AUDIENCE = None
# A SQL dataset health check. Note if enabled it is strongly advised that the callable
# be memoized to aid with performance, i.e.,

View File

@ -74,6 +74,7 @@ from superset.security.guest_token import (
GuestUser,
)
from superset.utils.core import DatasourceName, RowLevelSecurityFilterType
from superset.utils.urls import get_url_host
if TYPE_CHECKING:
from superset.common.query_context import QueryContext
@ -1308,6 +1309,7 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
secret = current_app.config["GUEST_TOKEN_JWT_SECRET"]
algo = current_app.config["GUEST_TOKEN_JWT_ALGO"]
exp_seconds = current_app.config["GUEST_TOKEN_JWT_EXP_SECONDS"]
aud = current_app.config["GUEST_TOKEN_JWT_AUDIENCE"] or get_url_host()
# calculate expiration time
now = self._get_current_epoch_time()
@ -1319,6 +1321,8 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
# standard jwt claims:
"iat": now, # issued at
"exp": exp, # expiration time
"aud": aud,
"type": "guest",
}
token = jwt.encode(claims, secret, algorithm=algo)
return token
@ -1344,6 +1348,8 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
raise ValueError("Guest token does not contain a resources claim")
if token.get("rls_rules") is None:
raise ValueError("Guest token does not contain an rls_rules claim")
if token.get("type") != "guest":
raise ValueError("This is not a guest token.")
except Exception: # pylint: disable=broad-except
# The login manager will handle sending 401s.
# We don't need to send a special error message.
@ -1366,7 +1372,8 @@ class SupersetSecurityManager( # pylint: disable=too-many-public-methods
"""
secret = current_app.config["GUEST_TOKEN_JWT_SECRET"]
algo = current_app.config["GUEST_TOKEN_JWT_ALGO"]
return jwt.decode(raw_token, secret, algorithms=[algo])
aud = current_app.config["GUEST_TOKEN_JWT_AUDIENCE"] or get_url_host()
return jwt.decode(raw_token, secret, algorithms=[algo], audience=aud)
@staticmethod
def is_guest_user(user: Optional[Any] = None) -> bool:

View File

@ -22,6 +22,7 @@ import jwt
from tests.integration_tests.base_tests import SupersetTestCase
from flask_wtf.csrf import generate_csrf
from superset.utils.urls import get_url_host
class TestSecurityCsrfApi(SupersetTestCase):
@ -90,6 +91,8 @@ class TestSecurityGuestTokenApi(SupersetTestCase):
self.assert200(response)
token = json.loads(response.data)["token"]
decoded_token = jwt.decode(token, self.app.config["GUEST_TOKEN_JWT_SECRET"])
decoded_token = jwt.decode(
token, self.app.config["GUEST_TOKEN_JWT_SECRET"], audience=get_url_host()
)
self.assertEqual(user, decoded_token["user"])
self.assertEqual(resource, decoded_token["resources"][0])

View File

@ -44,6 +44,7 @@ from superset.utils.core import (
get_example_default_schema,
)
from superset.utils.database import get_example_database
from superset.utils.urls import get_url_host
from superset.views.access_requests import AccessRequestsModelView
from .base_tests import SupersetTestCase
@ -1177,17 +1178,20 @@ class TestGuestTokens(SupersetTestCase):
resources = [{"some": "resource"}]
rls = [{"dataset": 1, "clause": "access = 1"}]
token = security_manager.create_guest_access_token(user, resources, rls)
aud = get_url_host()
# unfortunately we cannot mock time in the jwt lib
decoded_token = jwt.decode(
token,
self.app.config["GUEST_TOKEN_JWT_SECRET"],
algorithms=[self.app.config["GUEST_TOKEN_JWT_ALGO"]],
audience=aud,
)
self.assertEqual(user, decoded_token["user"])
self.assertEqual(resources, decoded_token["resources"])
self.assertEqual(now, decoded_token["iat"])
self.assertEqual(aud, decoded_token["aud"])
self.assertEqual("guest", decoded_token["type"])
self.assertEqual(
now + (self.app.config["GUEST_TOKEN_JWT_EXP_SECONDS"] * 1000),
decoded_token["exp"],
@ -1241,3 +1245,57 @@ class TestGuestTokens(SupersetTestCase):
self.assertRaisesRegex(
ValueError, "Guest token does not contain a resources claim"
)
def test_get_guest_user_not_guest_type(self):
now = time.time()
user = {"username": "test_guest"}
resources = [{"some": "resource"}]
aud = get_url_host()
claims = {
"user": user,
"resources": resources,
"rls_rules": [],
# standard jwt claims:
"aud": aud,
"iat": now, # issued at
"type": "not_guest",
}
token = jwt.encode(
claims,
self.app.config["GUEST_TOKEN_JWT_SECRET"],
algorithm=self.app.config["GUEST_TOKEN_JWT_ALGO"],
)
fake_request = FakeRequest()
fake_request.headers[current_app.config["GUEST_TOKEN_HEADER_NAME"]] = token
guest_user = security_manager.get_guest_user_from_request(fake_request)
self.assertIsNone(guest_user)
self.assertRaisesRegex(ValueError, "This is not a guest token.")
def test_get_guest_user_bad_audience(self):
now = time.time()
user = {"username": "test_guest"}
resources = [{"some": "resource"}]
aud = get_url_host()
claims = {
"user": user,
"resources": resources,
"rls_rules": [],
# standard jwt claims:
"aud": "bad_audience",
"iat": now, # issued at
"type": "guest",
}
token = jwt.encode(
claims,
self.app.config["GUEST_TOKEN_JWT_SECRET"],
algorithm=self.app.config["GUEST_TOKEN_JWT_ALGO"],
)
fake_request = FakeRequest()
fake_request.headers[current_app.config["GUEST_TOKEN_HEADER_NAME"]] = token
guest_user = security_manager.get_guest_user_from_request(fake_request)
self.assertRaisesRegex(jwt.exceptions.InvalidAudienceError, "Invalid audience")
self.assertIsNone(guest_user)