Skip to content

Commit

Permalink
Add token rotation test pattern and enable WebClient customization
Browse files Browse the repository at this point in the history
  • Loading branch information
seratch committed Jan 15, 2022
1 parent 136b22b commit f68586a
Show file tree
Hide file tree
Showing 7 changed files with 260 additions and 27 deletions.
1 change: 1 addition & 0 deletions slack_bolt/app/app.py
Expand Up @@ -231,6 +231,7 @@ def message_hello(message, say):
client_secret=settings.client_secret if settings is not None else None,
logger=self._framework_logger,
bot_only=installation_store_bot_only,
client=self._client, # for proxy use cases etc.
)

self._oauth_flow: Optional[OAuthFlow] = None
Expand Down
1 change: 1 addition & 0 deletions slack_bolt/app/async_app.py
Expand Up @@ -239,6 +239,7 @@ async def message_hello(message, say): # async function
client_secret=settings.client_secret if settings is not None else None,
logger=self._framework_logger,
bot_only=installation_store_bot_only,
client=self._async_client, # for proxy use cases etc.
)

self._async_oauth_flow: Optional[AsyncOAuthFlow] = None
Expand Down
3 changes: 3 additions & 0 deletions slack_bolt/authorization/async_authorize.py
Expand Up @@ -8,6 +8,7 @@
AsyncInstallationStore,
)
from slack_sdk.oauth.token_rotation.async_rotator import AsyncTokenRotator
from slack_sdk.web.async_client import AsyncWebClient

from slack_bolt.authorization.async_authorize_args import AsyncAuthorizeArgs
from slack_bolt.authorization import AuthorizeResult
Expand Down Expand Up @@ -124,6 +125,7 @@ def __init__(
# use only InstallationStore#find_bot(enterprise_id, team_id)
bot_only: bool = False,
cache_enabled: bool = False,
client: Optional[AsyncWebClient] = None,
):
self.logger = logger
self.installation_store = installation_store
Expand All @@ -136,6 +138,7 @@ def __init__(
self.token_rotator = AsyncTokenRotator(
client_id=client_id,
client_secret=client_secret,
client=client,
)
else:
self.token_rotator = None
Expand Down
3 changes: 3 additions & 0 deletions slack_bolt/authorization/authorize.py
Expand Up @@ -7,6 +7,7 @@
from slack_sdk.oauth.installation_store.models.bot import Bot
from slack_sdk.oauth.installation_store.models.installation import Installation
from slack_sdk.oauth.token_rotation.rotator import TokenRotator
from slack_sdk.web import WebClient

from slack_bolt.authorization.authorize_args import AuthorizeArgs
from slack_bolt.authorization.authorize_result import AuthorizeResult
Expand Down Expand Up @@ -129,6 +130,7 @@ def __init__(
# use only InstallationStore#find_bot(enterprise_id, team_id)
bot_only: bool = False,
cache_enabled: bool = False,
client: Optional[WebClient] = None,
):
self.logger = logger
self.installation_store = installation_store
Expand All @@ -143,6 +145,7 @@ def __init__(
self.token_rotator = TokenRotator(
client_id=client_id,
client_secret=client_secret,
client=client,
)
else:
self.token_rotator = None
Expand Down
114 changes: 87 additions & 27 deletions tests/mock_web_api_server.py
Expand Up @@ -6,7 +6,7 @@
import time
from http import HTTPStatus
from http.server import HTTPServer, SimpleHTTPRequestHandler
from typing import Type
from typing import Type, Optional
from unittest import TestCase
from urllib.parse import urlparse, parse_qs, ParseResult

Expand Down Expand Up @@ -65,7 +65,46 @@ def set_common_headers(self):
"token_type": "user"
}
}
"""
"""
oauth_v2_access_bot_refresh_response = """
{
"ok": true,
"app_id": "A0KRD7HC3",
"access_token": "xoxb-valid-refreshed",
"expires_in": 43200,
"refresh_token": "xoxe-1-valid-bot-refreshed",
"token_type": "bot",
"scope": "chat:write,commands",
"bot_user_id": "U0KRQLJ9H",
"team": {
"name": "Slack Softball Team",
"id": "T9TK3CUKW"
},
"enterprise": {
"name": "slack-sports",
"id": "E12345678"
}
}
"""
oauth_v2_access_user_refresh_response = """
{
"ok": true,
"app_id": "A0KRD7HC3",
"access_token": "xoxp-valid-refreshed",
"expires_in": 43200,
"refresh_token": "xoxe-1-valid-user-refreshed",
"token_type": "user",
"scope": "search:read",
"team": {
"name": "Slack Softball Team",
"id": "T9TK3CUKW"
},
"enterprise": {
"name": "slack-sports",
"id": "E12345678"
}
}
"""
bot_auth_test_response = """
{
"ok": true,
Expand Down Expand Up @@ -108,10 +147,31 @@ def _handle(self):

body = {"ok": True}
if path == "/oauth.v2.access":
self.send_response(200)
self.set_common_headers()
self.wfile.write(self.oauth_v2_access_response.encode("utf-8"))
return
if self.headers.get("authorization") is not None:
request_body = self._parse_request_body(
parsed_path=parsed_path,
content_len=int(self.headers.get("Content-Length") or 0),
)
self.logger.info(f"request body: {request_body}")

if request_body.get("grant_type") == "refresh_token":
if "bot-valid" in request_body.get("refresh_token"):
self.send_response(200)
self.set_common_headers()
body = self.oauth_v2_access_bot_refresh_response
self.wfile.write(body.encode("utf-8"))
return
if "user-valid" in request_body.get("refresh_token"):
self.send_response(200)
self.set_common_headers()
body = self.oauth_v2_access_user_refresh_response
self.wfile.write(body.encode("utf-8"))
return
if request_body.get("code") is not None:
self.send_response(200)
self.set_common_headers()
self.wfile.write(self.oauth_v2_access_response.encode("utf-8"))
return

if self.is_valid_user_token():
if path == "/auth.test":
Expand All @@ -127,27 +187,10 @@ def _handle(self):
self.wfile.write(self.bot_auth_test_response.encode("utf-8"))
return

len_header = self.headers.get("Content-Length") or 0
content_len = int(len_header)
post_body = self.rfile.read(content_len)
request_body = None
if post_body:
try:
post_body = post_body.decode("utf-8")
if post_body.startswith("{"):
request_body = json.loads(post_body)
else:
request_body = {
k: v[0] for k, v in parse_qs(post_body).items()
}
except UnicodeDecodeError:
pass
else:
if parsed_path and parsed_path.query:
request_body = {
k: v[0] for k, v in parse_qs(parsed_path.query).items()
}

request_body = self._parse_request_body(
parsed_path=parsed_path,
content_len=int(self.headers.get("Content-Length") or 0),
)
self.logger.info(f"request: {path} {request_body}")

header = self.headers["authorization"]
Expand Down Expand Up @@ -175,6 +218,23 @@ def do_GET(self):
def do_POST(self):
self._handle()

def _parse_request_body(self, parsed_path: str, content_len: int) -> Optional[dict]:
post_body = self.rfile.read(content_len)
request_body = None
if post_body:
try:
post_body = post_body.decode("utf-8")
if post_body.startswith("{"):
request_body = json.loads(post_body)
else:
request_body = {k: v[0] for k, v in parse_qs(post_body).items()}
except UnicodeDecodeError:
pass
else:
if parsed_path and parsed_path.query:
request_body = {k: v[0] for k, v in parse_qs(parsed_path.query).items()}
return request_body


#
# multiprocessing
Expand Down
82 changes: 82 additions & 0 deletions tests/slack_bolt/authorization/test_authorize.py
Expand Up @@ -10,6 +10,7 @@

from slack_bolt import BoltContext
from slack_bolt.authorization.authorize import InstallationStoreAuthorize, Authorize
from slack_bolt.error import BoltError
from tests.mock_web_api_server import (
cleanup_mock_web_api_server,
setup_mock_web_api_server,
Expand Down Expand Up @@ -211,6 +212,39 @@ def test_fetch_different_user_token(self):
assert result.user_token == "xoxp-valid"
assert_auth_test_count(self, 1)

def test_fetch_different_user_token_with_rotation(self):
context = BoltContext()
mock_client = WebClient(base_url=self.mock_api_server_base_url)
context["client"] = mock_client

installation_store = ValidUserTokenRotationInstallationStore()
invalid_authorize = InstallationStoreAuthorize(
logger=installation_store.logger, installation_store=installation_store
)
with pytest.raises(BoltError):
invalid_authorize(
context=context,
enterprise_id="E111",
team_id="T0G9PQBBK",
user_id="W222",
)

authorize = InstallationStoreAuthorize(
client_id="111.222",
client_secret="secret",
client=mock_client,
logger=installation_store.logger,
installation_store=installation_store,
)
result = authorize(
context=context, enterprise_id="E111", team_id="T0G9PQBBK", user_id="W222"
)
assert result.bot_id == "BZYBOTHED"
assert result.bot_user_id == "W23456789"
assert result.bot_token == "xoxb-valid-refreshed"
assert result.user_token == "xoxp-valid-refreshed"
assert_auth_test_count(self, 1)


class LegacyMemoryInstallationStore(InstallationStore):
@property
Expand Down Expand Up @@ -315,3 +349,51 @@ def find_installation(
user_scopes=["search:read"],
installed_at=datetime.datetime.now().timestamp(),
)


class ValidUserTokenRotationInstallationStore(InstallationStore):
@property
def logger(self) -> Logger:
return logging.getLogger(__name__)

def save(self, installation: Installation):
pass

def find_installation(
self,
*,
enterprise_id: Optional[str],
team_id: Optional[str],
user_id: Optional[str] = None,
is_enterprise_install: Optional[bool] = False,
) -> Optional[Installation]:
if user_id is None:
return Installation(
app_id="A111",
enterprise_id="E111",
team_id="T0G9PQBBK",
bot_token="xoxb-valid",
bot_refresh_token="xoxe-bot-valid",
bot_token_expires_in=-10,
bot_id="B",
bot_user_id="W",
bot_scopes=["commands", "chat:write"],
user_id="W11111",
user_token="xoxp-different-installer",
user_refresh_token="xoxe-1-user-valid",
user_token_expires_in=-10,
user_scopes=["search:read"],
installed_at=datetime.datetime.now().timestamp(),
)
elif user_id == "W222":
return Installation(
app_id="A111",
enterprise_id="E111",
team_id="T0G9PQBBK",
user_id="W222",
user_token="xoxp-valid",
user_refresh_token="xoxe-1-user-valid",
user_token_expires_in=-10,
user_scopes=["search:read"],
installed_at=datetime.datetime.now().timestamp(),
)

0 comments on commit f68586a

Please sign in to comment.