-
Notifications
You must be signed in to change notification settings - Fork 855
Expand file tree
/
Copy pathoauth_v2_async.py
More file actions
211 lines (185 loc) · 8.3 KB
/
oauth_v2_async.py
File metadata and controls
211 lines (185 loc) · 8.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
# ---------------------
# Sanic App for Slack OAuth flow
# ---------------------
import html
import logging
import os
from slack_sdk.web.async_client import AsyncWebClient
from slack_sdk.oauth import AuthorizeUrlGenerator, RedirectUriPageRenderer
from slack_sdk.oauth.installation_store import FileInstallationStore, Installation
from slack_sdk.oauth.state_store import FileOAuthStateStore
client_id = os.environ["SLACK_CLIENT_ID"]
client_secret = os.environ["SLACK_CLIENT_SECRET"]
scopes = ["app_mentions:read", "chat:write"]
user_scopes = ["search:read"]
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.DEBUG)
state_store = FileOAuthStateStore(expiration_seconds=300)
installation_store = FileInstallationStore()
authorization_url_generator = AuthorizeUrlGenerator(
client_id=client_id,
scopes=scopes,
user_scopes=user_scopes,
)
redirect_page_renderer = RedirectUriPageRenderer(
install_path="/slack/install",
redirect_uri_path="/slack/oauth_redirect",
)
# https://sanicframework.org/
from sanic import Sanic
from sanic.response import json
from sanic.request import Request
from sanic.response import HTTPResponse
app = Sanic("my-awesome-slack-app")
@app.get("/slack/install")
async def oauth_start(req: Request):
state = state_store.issue()
url = authorization_url_generator.generate(state)
return HTTPResponse(
status=200,
body=f'<a href="{html.escape(url)}">'
f'<img alt=""Add to Slack"" height="40" width="139" src="https://platform.slack-edge.com/img/add_to_slack.png" srcset="https://platform.slack-edge.com/img/add_to_slack.png 1x, https://platform.slack-edge.com/img/add_to_slack@2x.png 2x" /></a>',
)
@app.get("/slack/oauth_redirect")
async def oauth_callback(req: Request):
# Retrieve the auth code and state from the request params
if "code" in req.args:
state = req.args.get("state")
if state_store.consume(state):
code = req.args.get("code")
client = AsyncWebClient() # no prepared token needed for this app
oauth_response = await client.oauth_v2_access(client_id=client_id, client_secret=client_secret, code=code)
logger.info(f"oauth.v2.access response: {oauth_response}")
installed_enterprise = oauth_response.get("enterprise") or {}
is_enterprise_install = oauth_response.get("is_enterprise_install")
installed_team = oauth_response.get("team") or {}
installer = oauth_response.get("authed_user") or {}
incoming_webhook = oauth_response.get("incoming_webhook") or {}
bot_token = oauth_response.get("access_token")
# NOTE: oauth.v2.access doesn't include bot_id in response
bot_id = None
if bot_token is not None:
auth_test = await client.auth_test(token=bot_token)
bot_id = auth_test["bot_id"]
installation = Installation(
app_id=oauth_response.get("app_id"),
enterprise_id=installed_enterprise.get("id"),
team_id=installed_team.get("id"),
bot_token=bot_token,
bot_id=bot_id,
bot_user_id=oauth_response.get("bot_user_id"),
bot_scopes=oauth_response.get("scope"), # comma-separated string
user_id=installer.get("id"),
user_token=installer.get("access_token"),
user_scopes=installer.get("scope"), # comma-separated string
incoming_webhook_url=incoming_webhook.get("url"),
incoming_webhook_channel_id=incoming_webhook.get("channel_id"),
incoming_webhook_configuration_url=incoming_webhook.get("configuration_url"),
is_enterprise_install=is_enterprise_install,
token_type=oauth_response.get("token_type"),
)
installation_store.save(installation)
html = redirect_page_renderer.render_success_page(
app_id=installation.app_id,
team_id=installation.team_id,
is_enterprise_install=installation.is_enterprise_install,
enterprise_url=installation.enterprise_url,
)
return HTTPResponse(
status=200,
headers={
"Content-Type": "text/html; charset=utf-8",
},
body=html,
)
else:
html = redirect_page_renderer.render_failure_page("the state value is already expired")
return HTTPResponse(
status=400,
headers={
"Content-Type": "text/html; charset=utf-8",
},
body=html,
)
error = req.args.get("error") if "error" in req.args else ""
return HTTPResponse(
status=400,
headers={"Content-Type": "text/html; charset=utf-8"},
body=redirect_page_renderer.render_failure_page(error),
)
# ---------------------
# Sanic App for Slack events
# ---------------------
import json
from slack_sdk.errors import SlackApiError
from slack_sdk.signature import SignatureVerifier
signing_secret = os.environ["SLACK_SIGNING_SECRET"]
signature_verifier = SignatureVerifier(signing_secret=signing_secret)
@app.post("/slack/events")
async def slack_app(req: Request):
data = req.body.decode("utf-8")
if not signature_verifier.is_valid(
body=data,
timestamp=req.headers.get("X-Slack-Request-Timestamp"),
signature=req.headers.get("X-Slack-Signature"),
):
return HTTPResponse(status=403, body="invalid request")
if data and "url_verification" in data:
body = json.loads(data)
if body.get("type") == "url_verification" and "challenge" in body:
return HTTPResponse(status=200, body=body["challenge"])
if "command" in req.form and req.form.get("command") == "/open-modal":
try:
enterprise_id = req.form.get("enterprise_id")
team_id = req.form.get("team_id")
bot = installation_store.find_bot(
enterprise_id=enterprise_id,
team_id=team_id,
)
bot_token = bot.bot_token if bot else None
if not bot_token:
return HTTPResponse(status=200, body="Please install this app first!")
client = AsyncWebClient(token=bot_token)
await client.views_open(
trigger_id=req.form.get("trigger_id"),
view={
"type": "modal",
"callback_id": "modal-id",
"title": {"type": "plain_text", "text": "Awesome Modal"},
"submit": {"type": "plain_text", "text": "Submit"},
"close": {"type": "plain_text", "text": "Cancel"},
"blocks": [
{
"type": "input",
"block_id": "b-id",
"label": {
"type": "plain_text",
"text": "Input label",
},
"element": {
"action_id": "a-id",
"type": "plain_text_input",
},
}
],
},
)
return HTTPResponse(status=200, body="")
except SlackApiError as e:
code = e.response["error"]
return HTTPResponse(status=200, body=f"Failed to open a modal due to {code}")
elif "payload" in req.form:
payload = json.loads(req.form.get("payload"))
if payload.get("type") == "view_submission" and payload.get("view").get("callback_id") == "modal-id":
submitted_data = payload.get("view").get("state").get("values")
print(submitted_data) # {'b-id': {'a-id': {'type': 'plain_text_input', 'value': 'your input'}}}
return HTTPResponse(status=200, body="")
return HTTPResponse(status=404, body="Not found")
if __name__ == "__main__":
# export SLACK_CLIENT_ID=123.123
# export SLACK_CLIENT_SECRET=xxx
# export SLACK_SIGNING_SECRET=***
app.run(host="0.0.0.0", port=3000)
# python3 integration_tests/samples/oauth/oauth_v2_async.py
# ngrok http 3000
# https://{yours}.ngrok.io/slack/install