def test_installation_store_conflicts(self): store1 = FileInstallationStore() store2 = FileInstallationStore() app = AsyncApp( signing_secret="valid", oauth_settings=AsyncOAuthSettings( client_id="111.222", client_secret="valid", installation_store=store1, ), installation_store=store2, ) assert app.installation_store is store1 app = AsyncApp( signing_secret="valid", oauth_flow=AsyncOAuthFlow( settings=AsyncOAuthSettings( client_id="111.222", client_secret="valid", installation_store=store1, ) ), installation_store=store2, ) assert app.installation_store is store1 app = AsyncApp( signing_secret="valid", oauth_flow=AsyncOAuthFlow( settings=AsyncOAuthSettings(client_id="111.222", client_secret="valid",) ), installation_store=store1, ) assert app.installation_store is store1
def __init__( self, *, client: Optional[AsyncWebClient] = None, logger: Optional[Logger] = None, settings: AsyncOAuthSettings, ): """The module to run the Slack app installation flow (OAuth flow). :param client: The AsyncWebClient. :param logger: The logger. :param settings: OAuth settings to configure this module. """ self._async_client = client self._logger = logger self.settings = settings self.settings.logger = self._logger self.client_id = self.settings.client_id self.redirect_uri = self.settings.redirect_uri self.install_path = self.settings.install_path self.redirect_uri_path = self.settings.redirect_uri_path self.default_callback_options = DefaultAsyncCallbackOptions( logger=logger, state_utils=self.settings.state_utils, redirect_uri_page_renderer=self.settings. redirect_uri_page_renderer, ) if settings.callback_options is None: settings.callback_options = self.default_callback_options self.success_handler = settings.callback_options.success self.failure_handler = settings.callback_options.failure
def test_oauth(self): app = AsyncApp( client=self.web_client, signing_secret=self.signing_secret, oauth_settings=AsyncOAuthSettings( client_id="111.111", client_secret="xxx", scopes=["chat:write", "commands"], ), ) app_handler = AsyncSlackRequestHandler(app) async def endpoint(req: Request): return await app_handler.handle(req) api = Starlette( debug=True, routes=[ Route("/slack/install", endpoint=endpoint, methods=["GET"]) ], ) client = TestClient(api) response = client.get("/slack/install", allow_redirects=False) assert response.status_code == 200 assert response.headers.get( "content-type") == "text/html; charset=utf-8" assert response.headers.get("content-length") == "565" assert "https://slack.com/oauth/v2/authorize?state=" in response.text
def test_authorize_conflicts(self): oauth_settings = AsyncOAuthSettings( client_id="111.222", client_secret="valid", installation_store=FileInstallationStore(), state_store=FileOAuthStateStore(expiration_seconds=120), ) # no error with this AsyncApp(signing_secret="valid", oauth_settings=oauth_settings) def authorize() -> AuthorizeResult: return AuthorizeResult(enterprise_id="E111", team_id="T111") with pytest.raises(BoltError): AsyncApp( signing_secret="valid", authorize=authorize, oauth_settings=oauth_settings, ) oauth_flow = AsyncOAuthFlow(settings=oauth_settings) # no error with this AsyncApp(signing_secret="valid", oauth_flow=oauth_flow) with pytest.raises(BoltError): AsyncApp(signing_secret="valid", authorize=authorize, oauth_flow=oauth_flow)
async def test_handle_callback(self): oauth_flow = AsyncOAuthFlow( client=AsyncWebClient(base_url=self.mock_api_server_base_url), settings=AsyncOAuthSettings( client_id="111.222", client_secret="xxx", scopes=["chat:write", "commands"], installation_store=FileInstallationStore(), state_store=FileOAuthStateStore(expiration_seconds=120), success_url="https://www.example.com/completion", failure_url="https://www.example.com/failure", ), ) state = await oauth_flow.issue_new_state(None) req = AsyncBoltRequest( body="", query=f"code=foo&state={state}", headers={ "cookie": [f"{oauth_flow.settings.state_cookie_name}={state}"] }, ) resp = await oauth_flow.handle_callback(req) assert resp.status == 200 assert "https://www.example.com/completion" in resp.body app = AsyncApp(signing_secret="signing_secret", oauth_flow=oauth_flow) global_shortcut_body = { "type": "shortcut", "token": "verification_token", "action_ts": "111.111", "team": { "id": "T111", "domain": "workspace-domain", "enterprise_id": "E111", "enterprise_name": "Org Name", }, "user": { "id": "W111", "username": "******", "team_id": "T111" }, "callback_id": "test-shortcut", "trigger_id": "111.111.xxxxxx", } body = f"payload={quote(json.dumps(global_shortcut_body))}" timestamp = str(int(time())) signature_verifier = SignatureVerifier("signing_secret") headers = { "content-type": ["application/x-www-form-urlencoded"], "x-slack-signature": [ signature_verifier.generate_signature(body=body, timestamp=timestamp) ], "x-slack-request-timestamp": [timestamp], } request = AsyncBoltRequest(body=body, headers=headers) response = await app.async_dispatch(request) assert response.status == 200 await assert_auth_test_count_async(self, 1)
def test_valid_multi_auth(self): app = AsyncApp( signing_secret="valid", oauth_settings=AsyncOAuthSettings( client_id="111.222", client_secret="valid" ), ) assert app != None
def test_valid_multi_auth_secret_absence(self): with pytest.raises(BoltError): AsyncApp( signing_secret="valid", oauth_settings=AsyncOAuthSettings( client_id="111.222", client_secret=None ), )
async def test_scopes_as_str(self): settings = AsyncOAuthSettings( client_id="111.222", client_secret="xxx", scopes="chat:write,commands", user_scopes="search:read", ) assert settings.scopes == ["chat:write", "commands"] assert settings.user_scopes == ["search:read"]
def test_valid_multi_auth_oauth_flow(self): oauth_flow = AsyncOAuthFlow( settings=AsyncOAuthSettings( client_id="111.222", client_secret="valid", installation_store=FileInstallationStore(), state_store=FileOAuthStateStore(expiration_seconds=120), ) ) app = AsyncApp(signing_secret="valid", oauth_flow=oauth_flow) assert app != None
async def test_instantiation(self): oauth_flow = AsyncOAuthFlow(settings=AsyncOAuthSettings( client_id="111.222", client_secret="xxx", scopes=["chat:write", "commands"], user_scopes=["search:read"], installation_store=FileInstallationStore(), state_store=FileOAuthStateStore(expiration_seconds=120), )) assert oauth_flow is not None assert oauth_flow.logger is not None assert oauth_flow.client is not None
async def test_handle_installation_default(self): oauth_flow = AsyncOAuthFlow(settings=AsyncOAuthSettings( client_id="111.222", client_secret="xxx", scopes=["chat:write", "commands"], installation_store=FileInstallationStore(), state_store=FileOAuthStateStore(expiration_seconds=120), )) req = AsyncBoltRequest(body="") resp = await oauth_flow.handle_installation(req) assert resp.status == 200 assert resp.headers.get("content-type") == ["text/html; charset=utf-8"] assert "https://slack.com/oauth/v2/authorize?state=" in resp.body
async def test_handle_installation_no_rendering(self): oauth_flow = AsyncOAuthFlow(settings=AsyncOAuthSettings( client_id="111.222", client_secret="xxx", scopes=["chat:write", "commands"], installation_store=FileInstallationStore(), install_page_rendering_enabled=False, # disabled state_store=FileOAuthStateStore(expiration_seconds=120), )) req = AsyncBoltRequest(body="") resp = await oauth_flow.handle_installation(req) assert resp.status == 302 location_header = resp.headers.get("location")[0] assert "https://slack.com/oauth/v2/authorize?state=" in location_header
async def test_handle_callback_using_options(self): async def success(args: AsyncSuccessArgs) -> BoltResponse: assert args.request is not None return BoltResponse(status=200, body="customized") async def failure(args: AsyncFailureArgs) -> BoltResponse: assert args.request is not None assert args.reason is not None return BoltResponse(status=502, body="customized") oauth_flow = AsyncOAuthFlow( client=AsyncWebClient(base_url=self.mock_api_server_base_url), settings=AsyncOAuthSettings( client_id="111.222", client_secret="xxx", scopes=["chat:write", "commands"], installation_store=FileInstallationStore(), state_store=FileOAuthStateStore(expiration_seconds=120), callback_options=AsyncCallbackOptions( success=success, failure=failure, ), ), ) state = await oauth_flow.issue_new_state(None) req = AsyncBoltRequest( body="", query=f"code=foo&state={state}", headers={ "cookie": [f"{oauth_flow.settings.state_cookie_name}={state}"] }, ) resp = await oauth_flow.handle_callback(req) assert resp.status == 200 assert resp.body == "customized" req = AsyncBoltRequest( body="", query=f"code=foo&state=invalid", headers={ "cookie": [f"{oauth_flow.settings.state_cookie_name}={state}"] }, ) resp = await oauth_flow.handle_callback(req) assert resp.status == 502 assert resp.body == "customized"
async def test_handle_callback_invalid_state(self): oauth_flow = AsyncOAuthFlow(settings=AsyncOAuthSettings( client_id="111.222", client_secret="xxx", scopes=["chat:write", "commands"], installation_store=FileInstallationStore(), state_store=FileOAuthStateStore(expiration_seconds=120), )) state = await oauth_flow.issue_new_state(None) req = AsyncBoltRequest( body="", query=f"code=foo&state=invalid", headers={ "cookie": [f"{oauth_flow.settings.state_cookie_name}={state}"] }, ) resp = await oauth_flow.handle_callback(req) assert resp.status == 400
def __init__( self, *, client: Optional[AsyncWebClient] = None, logger: Optional[Logger] = None, settings: AsyncOAuthSettings, ): """The module to run the Slack app installation flow (OAuth flow). Args: client: The `slack_sdk.web.async_client.AsyncWebClient` instance. logger: The logger. settings: OAuth settings to configure this module. """ self._async_client = client self._logger = logger if not isinstance(settings, AsyncOAuthSettings): raise BoltError(error_oauth_settings_invalid_type_async()) self.settings = settings self.settings.logger = self._logger self.client_id = self.settings.client_id self.redirect_uri = self.settings.redirect_uri self.install_path = self.settings.install_path self.redirect_uri_path = self.settings.redirect_uri_path self.default_callback_options = DefaultAsyncCallbackOptions( logger=logger, state_utils=self.settings.state_utils, redirect_uri_page_renderer=self.settings. redirect_uri_page_renderer, ) if settings.callback_options is None: settings.callback_options = self.default_callback_options self.success_handler = settings.callback_options.success self.failure_handler = settings.callback_options.failure
async def test_oauth(self): app = AsyncApp( client=self.web_client, signing_secret=self.signing_secret, oauth_settings=AsyncOAuthSettings( client_id="111.111", client_secret="xxx", scopes=["chat:write", "commands"], ), ) api = Sanic(name=self.unique_sanic_app_name()) app_handler = AsyncSlackRequestHandler(app) @api.get("/slack/install") async def endpoint(req: Request): return await app_handler.handle(req) _, response = await api.asgi_client.get( url="/slack/install", allow_redirects=False ) assert response.status_code == 200 assert response.headers.get("content-type") == "text/html; charset=utf-8" assert response.headers.get("content-length") == "565" assert "https://slack.com/oauth/v2/authorize?state=" in response.text
# ------------------ import logging logging.basicConfig(level=logging.DEBUG) import os from slack_bolt.app.async_app import AsyncApp from slack_bolt.context.async_context import AsyncBoltContext from slack_bolt.oauth.async_oauth_settings import AsyncOAuthSettings app = AsyncApp(signing_secret=os.environ["SLACK_SIGNING_SECRET"], oauth_settings=AsyncOAuthSettings( client_id=os.environ["SLACK_CLIENT_ID"], client_secret=os.environ["SLACK_CLIENT_SECRET"], scopes=os.environ["SLACK_SCOPES"].split(","), )) @app.event("app_mention") async def mention(context: AsyncBoltContext): await context.say(":wave: Hi there!") @app.event("message") async def message(context: AsyncBoltContext, event: dict): await context.client.reactions_add( channel=event["channel"], timestamp=event["ts"], name="eyes",
user_scopes=["search:read"], installed_at=datetime.datetime.now().timestamp(), ) class BotOnlyMemoryInstallationStore(LegacyMemoryInstallationStore): async def async_find_installation( self, *, enterprise_id: Optional[str], team_id: Optional[str], user_id: Optional[str] = None, is_enterprise_install: Optional[bool] = False, ) -> Optional[Installation]: raise ValueError oauth_settings = AsyncOAuthSettings( client_id="111.222", client_secret="secret", installation_store=BotOnlyMemoryInstallationStore(), installation_store_bot_only=False, ) oauth_settings_bot_only = AsyncOAuthSettings( client_id="111.222", client_secret="secret", installation_store=BotOnlyMemoryInstallationStore(), installation_store_bot_only=True, )
def sqlite3( cls, database: str, # OAuth flow parameters/credentials authorization_url: Optional[str] = None, client_id: Optional[str] = None, # required client_secret: Optional[str] = None, # required scopes: Optional[Sequence[str]] = None, user_scopes: Optional[Sequence[str]] = None, redirect_uri: Optional[str] = None, # Handler configuration install_path: Optional[str] = None, redirect_uri_path: Optional[str] = None, callback_options: Optional[AsyncCallbackOptions] = None, success_url: Optional[str] = None, failure_url: Optional[str] = None, # Installation Management # state parameter related configurations state_cookie_name: str = OAuthStateUtils.default_cookie_name, state_expiration_seconds: int = OAuthStateUtils. default_expiration_seconds, client: Optional[AsyncWebClient] = None, logger: Optional[Logger] = None, ) -> "AsyncOAuthFlow": client_id = client_id or os.environ["SLACK_CLIENT_ID"] # required client_secret = client_secret or os.environ[ "SLACK_CLIENT_SECRET"] # required scopes = scopes or os.environ.get("SLACK_SCOPES", "").split(",") user_scopes = user_scopes or os.environ.get("SLACK_USER_SCOPES", "").split(",") redirect_uri = redirect_uri or os.environ.get("SLACK_REDIRECT_URI") return AsyncOAuthFlow( client=client or AsyncWebClient(), logger=logger, settings=AsyncOAuthSettings( # OAuth flow parameters/credentials authorization_url=authorization_url, client_id=client_id, client_secret=client_secret, scopes=scopes, user_scopes=user_scopes, redirect_uri=redirect_uri, # Handler configuration install_path=install_path, redirect_uri_path=redirect_uri_path, callback_options=callback_options, success_url=success_url, failure_url=failure_url, # Installation Management installation_store=SQLite3InstallationStore( database=database, client_id=client_id, logger=logger, ), # state parameter related configurations state_store=SQLite3OAuthStateStore( database=database, expiration_seconds=state_expiration_seconds, logger=logger, ), state_cookie_name=state_cookie_name, state_expiration_seconds=state_expiration_seconds, ), )
import sys import logging # Import the async app instead of the regular one from slack_bolt.async_app import AsyncApp from slack_bolt.oauth.async_oauth_settings import AsyncOAuthSettings from slack_sdk.oauth.installation_store import FileInstallationStore from slack_sdk.oauth.state_store import FileOAuthStateStore import carmille logging.basicConfig(level=logging.INFO) oauth_settings = AsyncOAuthSettings( client_id=os.environ["SLACK_CLIENT_ID"], client_secret=os.environ["SLACK_CLIENT_SECRET"], scopes=[ "channels:history", "channels:read", "commands", "emoji:read", "reactions:read", "users:read" ], installation_store=FileInstallationStore(base_dir="./data"), state_store=FileOAuthStateStore(expiration_seconds=600, base_dir="./data")) app = AsyncApp(signing_secret=os.environ["SLACK_SIGNING_SECRET"], oauth_settings=oauth_settings) @app.command("/carmille") async def command(context, ack, body, respond): await ack() user_tz_offset = await carmille.fetch.get_tz_offset( context.client, body['user_id'])
def __init__( self, *, logger: Optional[logging.Logger] = None, # Used in logger name: Optional[str] = None, # Set True when you run this app on a FaaS platform process_before_response: bool = False, # Basic Information > Credentials > Signing Secret signing_secret: Optional[str] = None, # for single-workspace apps token: Optional[str] = None, client: Optional[AsyncWebClient] = None, # for multi-workspace apps installation_store: Optional[AsyncInstallationStore] = None, authorize: Optional[Callable[..., Awaitable[AuthorizeResult]]] = None, # for the OAuth flow oauth_settings: Optional[AsyncOAuthSettings] = None, oauth_flow: Optional[AsyncOAuthFlow] = None, # No need to set (the value is used only in response to ssl_check requests) verification_token: Optional[str] = None, ): """Bolt App that provides functionalities to register middleware/listeners :param name: The application name that will be used in logging. If absent, the source file name will be used instead. :param process_before_response: True if this app runs on Function as a Service. (Default: False) :param signing_secret: The Signing Secret value used for verifying requests from Slack. :param token: The bot access token required only for single-workspace app. :param client: The singleton slack_sdk.web.async_client.AsyncWebClient instance for this app. :param installation_store: The module offering save/find operations of installation data :param authorize: The function to authorize an incoming request from Slack by checking if there is a team/user in the installation data. :param oauth_settings: The settings related to Slack app installation flow (OAuth flow) :param oauth_flow: Manually instantiated slack_bolt.oauth.async_oauth_flow.AsyncOAuthFlow. This is always prioritized over oauth_settings. :param verification_token: Deprecated verification mechanism. This can used only for ssl_check requests. """ signing_secret = signing_secret or os.environ.get( "SLACK_SIGNING_SECRET") token = token or os.environ.get("SLACK_BOT_TOKEN") self._name: str = name or inspect.stack()[1].filename.split( os.path.sep)[-1] self._signing_secret: str = signing_secret self._verification_token: Optional[ str] = verification_token or os.environ.get( "SLACK_VERIFICATION_TOKEN", None) self._framework_logger = logger or get_bolt_logger(AsyncApp) self._token: Optional[str] = token if client is not None: if not isinstance(client, AsyncWebClient): raise BoltError(error_client_invalid_type_async()) self._async_client = client self._token = client.token if token is not None: self._framework_logger.warning( warning_client_prioritized_and_token_skipped()) else: # NOTE: the token here can be None self._async_client = create_async_web_client(token) self._async_authorize: Optional[AsyncAuthorize] = None if authorize is not None: if oauth_settings is not None or oauth_flow is not None: raise BoltError(error_authorize_conflicts()) self._async_authorize = AsyncCallableAuthorize( logger=self._framework_logger, func=authorize) self._async_installation_store: Optional[ AsyncInstallationStore] = installation_store if self._async_installation_store is not None and self._async_authorize is None: self._async_authorize = AsyncInstallationStoreAuthorize( installation_store=self._async_installation_store, logger=self._framework_logger, ) self._async_oauth_flow: Optional[AsyncOAuthFlow] = None if (oauth_settings is None and os.environ.get("SLACK_CLIENT_ID") is not None and os.environ.get("SLACK_CLIENT_SECRET") is not None): # initialize with the default settings oauth_settings = AsyncOAuthSettings() if oauth_flow: if not isinstance(oauth_flow, AsyncOAuthFlow): raise BoltError(error_oauth_flow_invalid_type_async()) self._async_oauth_flow = oauth_flow installation_store = select_consistent_installation_store( client_id=self._async_oauth_flow.client_id, app_store=self._async_installation_store, oauth_flow_store=self._async_oauth_flow.settings. installation_store, logger=self._framework_logger, ) self._async_installation_store = installation_store self._async_oauth_flow.settings.installation_store = installation_store if self._async_oauth_flow._async_client is None: self._async_oauth_flow._async_client = self._async_client if self._async_authorize is None: self._async_authorize = self._async_oauth_flow.settings.authorize elif oauth_settings is not None: if not isinstance(oauth_settings, AsyncOAuthSettings): raise BoltError(error_oauth_settings_invalid_type_async()) installation_store = select_consistent_installation_store( client_id=oauth_settings.client_id, app_store=self._async_installation_store, oauth_flow_store=oauth_settings.installation_store, logger=self._framework_logger, ) self._async_installation_store = installation_store oauth_settings.installation_store = installation_store self._async_oauth_flow = AsyncOAuthFlow(client=self._async_client, logger=self.logger, settings=oauth_settings) if self._async_authorize is None: self._async_authorize = self._async_oauth_flow.settings.authorize if (self._async_installation_store is not None or self._async_authorize is not None) and self._token is not None: self._token = None self._framework_logger.warning(warning_token_skipped()) self._async_middleware_list: List[Union[Callable, AsyncMiddleware]] = [] self._async_listeners: List[AsyncListener] = [] self._async_listener_runner = AsyncioListenerRunner( logger=self._framework_logger, process_before_response=process_before_response, listener_error_handler=AsyncDefaultListenerErrorHandler( logger=self._framework_logger), lazy_listener_runner=AsyncioLazyListenerRunner( logger=self._framework_logger, ), ) self._init_middleware_list_done = False self._init_async_middleware_list() self._server: Optional[AsyncSlackAppServer] = None
database_url=database_url, logger=logger, ) oauth_state_store = AsyncSQLAlchemyOAuthStateStore( expiration_seconds=120, database_url=database_url, logger=logger, ) app = AsyncApp( logger=logger, signing_secret=signing_secret, installation_store=installation_store, oauth_settings=AsyncOAuthSettings( client_id=client_id, client_secret=client_secret, state_store=oauth_state_store, ), ) app_handler = AsyncSlackRequestHandler(app) @app.event("app_mention") async def handle_command(say: AsyncSay): await say("Hi!") from sanic import Sanic from sanic.request import Request api = Sanic(name="awesome-slack-app")