Skip to content
This repository was archived by the owner on Jun 26, 2025. It is now read-only.

Commit 4dc040b

Browse files
committed
Merge branch 'feature/sign-in' into develop
2 parents f03c0ba + b8873ea commit 4dc040b

File tree

8 files changed

+141
-10
lines changed

8 files changed

+141
-10
lines changed

Pipfile

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,27 @@ verify_ssl = true
77
pytest = "*"
88
pytest-cov = "*"
99
tox = "*"
10+
black = "*"
11+
flask-assistant = {editable = true,path = "."}
1012

1113
[packages]
12-
flask-assistant = {editable = true,path = "."}
14+
aniso8601 = "==4.0.1"
15+
certifi = "==2018.11.29"
16+
chardet = "==3.0.4"
17+
click = "==7.0"
18+
idna = "==2.8"
19+
itsdangerous = "==1.1.0"
20+
requests = "==2.21.0"
21+
urllib3 = "==1.24.1"
22+
Flask = "==1.0.2"
23+
Jinja2 = "==2.10"
24+
MarkupSafe = "==1.1.0"
25+
"ruamel.yaml" = "==0.15.81"
26+
Werkzeug = "==0.14.1"
27+
google-auth = "*"
28+
29+
[requires]
30+
python_version = "3.7"
1331

1432
[pipenv]
1533
allow_prereleases = true

flask_assistant/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,10 @@
2323
storage,
2424
session_id,
2525
context_in,
26+
profile
2627
)
2728

28-
from flask_assistant.response import ask, tell, event, build_item, permission
29+
from flask_assistant.response import ask, tell, event, build_item, permission, sign_in
2930
from flask_assistant.manager import Context
3031

3132
import flask_assistant.utils

flask_assistant/core.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ def find_assistant(): # Taken from Flask-ask courtesy of @voutilad
4040
session_id = LocalProxy(lambda: find_assistant().session_id)
4141
user = LocalProxy(lambda: find_assistant().user)
4242
storage = LocalProxy(lambda: find_assistant().storage)
43+
profile = LocalProxy(lambda: find_assistant().profile)
4344

4445
# Converter shorthands for commonly used system entities
4546
_converter_shorthands = {
@@ -65,6 +66,7 @@ class Assistant(object):
6566
blueprint {Flask Blueprint} -- Flask Blueprint instance to initialize (Default: {None})
6667
route {str} -- entry point to which initial Alexa Requests are forwarded (default: {None})
6768
project_id {str} -- Google Cloud Project ID, required to manage contexts from flask-assistant
69+
client_id {Str} -- Actions on Google client ID used for account linking
6870
dev_token {str} - Dialogflow dev access token used to register and retrieve agent resources
6971
client_token {str} - Dialogflow client access token required for querying agent
7072
"""
@@ -77,12 +79,14 @@ def __init__(
7779
project_id=None,
7880
dev_token=None,
7981
client_token=None,
82+
client_id=None,
8083
):
8184

8285
self.app = app
8386
self.blueprint = blueprint
8487
self._route = route
8588
self.project_id = project_id
89+
self.client_id = client_id
8690
self._intent_action_funcs = {}
8791
self._intent_mappings = {}
8892
self._intent_converts = {}
@@ -108,6 +112,9 @@ def __init__(
108112
"Assistant object must be intialized with either an app or blueprint"
109113
)
110114

115+
if self.client_id is None:
116+
self.client_id = self.app.config.get("AOG_CLIENT_ID")
117+
111118
if project_id is None:
112119
import warnings
113120

@@ -243,11 +250,19 @@ def storage(self):
243250

244251
@storage.setter
245252
def storage(self, value):
246-
if not isintance(value, dict):
253+
if not isinstance(value, dict):
247254
raise TypeError("Storage must be a dictionary")
248255

249256
self.user["userStorage"] = value
250257

258+
@property
259+
def profile(self):
260+
return getattr(_app_ctx_stack.top, "_assist_profile", None)
261+
262+
@profile.setter
263+
def profile(self, value):
264+
_app_ctx_stack.top._assist_profile = value
265+
251266
def _register_context_to_func(self, intent_name, context=[]):
252267
required = self._required_contexts.get(intent_name)
253268
if required:
@@ -372,6 +387,23 @@ def _dump_result(self, view_func, result):
372387
def _parse_session_id(self):
373388
return self.request["session"].split("/sessions/")[1]
374389

390+
def _set_user_profile(self):
391+
if self.client_id is None:
392+
return
393+
394+
if self.user.get("idToken") is not None:
395+
from flask_assistant.utils import decode_token
396+
397+
token = self.user["idToken"]
398+
profile_payload = decode_token(token, self.client_id)
399+
for k in ["sub", "iss", "aud", "iat", "exp"]:
400+
profile_payload.pop(k)
401+
402+
self.profile = profile_payload
403+
404+
405+
406+
375407
def _flask_assitant_view_func(self, nlp_result=None, *args, **kwargs):
376408
if nlp_result: # pass API query result directly
377409
self.request = nlp_result
@@ -401,6 +433,7 @@ def _flask_assitant_view_func(self, nlp_result=None, *args, **kwargs):
401433
payload = original_request.get("payload")
402434
if payload and payload.get("user"):
403435
self.user = original_request["payload"]["user"]
436+
self._set_user_profile()
404437

405438
# Get access token from request
406439
if original_request and original_request.get("user"):

flask_assistant/response.py

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -209,8 +209,11 @@ def build_item(
209209
}
210210

211211
if img_url:
212-
img_payload = {"imageUri": img_url, "accessibilityText": alt_text or "{} img".format(title)}
213-
item["image"] = img_payload
212+
img_payload = {
213+
"imageUri": img_url,
214+
"accessibilityText": alt_text or "{} img".format(title),
215+
}
216+
item["image"] = img_payload
214217

215218
return item
216219

@@ -355,3 +358,27 @@ def __init__(self, permissions, context=None, update_intent=None):
355358
},
356359
}
357360

361+
362+
class sign_in(_Response):
363+
"""Initiates the authentication flow for Account Linking
364+
365+
After the user authorizes the action to access their profile, a Google ID token
366+
will be received and validated by the flask-assistant and expose user profile information
367+
with the `user.profile` local
368+
369+
In order to complete the sign in process, you will need to create an intent with
370+
the `actions_intent_SIGN)IN` event
371+
"""
372+
373+
def __init__(self, reason=None):
374+
super(sign_in, self).__init__(speech=None)
375+
376+
self._messages[:] = []
377+
self._response["payload"]["google"]["systemIntent"] = {
378+
"intent": "actions.intent.SIGN_IN",
379+
"data": {
380+
"optContext": reason,
381+
"@type": "type.googleapis.com/google.actions.v2.SignInValueSpec",
382+
},
383+
}
384+

flask_assistant/utils.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,25 @@
11
from __future__ import absolute_import
2+
from typing import Dict, Any
23
import os
34
import sys
45
import logging
6+
from google.auth import jwt
57
from flask_assistant.core import Assistant
68
from . import logger
79

810

911
logger.setLevel(logging.INFO)
1012

13+
GOOGLE_PUBLIC_KEY = {
14+
"ee4dbd06c06683cb48dddca6b88c3e473b6915b9": "-----BEGIN CERTIFICATE-----\nMIIDJjCCAg6gAwIBAgIIXlp0tU/OdR8wDQYJKoZIhvcNAQEFBQAwNjE0MDIGA1UE\nAxMrZmVkZXJhdGVkLXNpZ25vbi5zeXN0ZW0uZ3NlcnZpY2VhY2NvdW50LmNvbTAe\nFw0xOTEwMDMxNDQ5MzRaFw0xOTEwMjAwMzA0MzRaMDYxNDAyBgNVBAMTK2ZlZGVy\nYXRlZC1zaWdub24uc3lzdGVtLmdzZXJ2aWNlYWNjb3VudC5jb20wggEiMA0GCSqG\nSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC41NLGPK9PRi0KjFTIQ9qEirje2IrmWSwZ\n7lmgTzwA4mpc4tqDn7AfUTHmuyhDrbweGq2wQeYJDbBPT5uX86XcQgAcu4IzSuZG\nJZ68ASYOWWlKV0vYjf6W/9v73sGJFxbkoAB8X7QH/fN80QYoXvSX+IwNnePnoikM\nnAsNiZrkLoqHuv5+ahOgpBN5qyvKglasNiXGpv8EL96CKb+nmMudzpypjbQHJUp2\nmfDvOiTX6IuSXyeYRkyzOeX7wqpV1l+TU3A8orMylNe8e+oL/2mAYVzCC9Wk1nq2\nGT4vRRmzrr2GW4eKr9525JQe7BKBKkC2WWhKE+EmqPm2ZFnQ/frlAgMBAAGjODA2\nMAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgeAMBYGA1UdJQEB/wQMMAoGCCsG\nAQUFBwMCMA0GCSqGSIb3DQEBBQUAA4IBAQA+mwyWb+SZMLOiQS1lUgN6iXO2JZm+\nq6rmSYnpLP5nCtwKjHbGSw0DoUchem2g0AYsB/HqNl5zLvJb5CXlP79sTO6Ot7sX\nHI3Mtqw4Fe1QFCC5QVhudpMNKNQYq8P45SrfwMW1qSYYsMXbpmNkIPbFvxib1L0l\niUfnjCXFB4XiDa80Cb73cxmU7a24/j03o42kjcRX2BtXs6jhP7z8BxnDCjybjlLT\no5gBombtlPKgOTdcF+eKdaO1LLQ+9LmueiZH/HCsvAmmxLT9g1XZCEFg5zttdetT\nVV+03sFBGhoJPbChiOJMdH8IQVEdtpvnAiYyVBYEUSj7CWSZEI50syL2\n-----END CERTIFICATE-----\n",
15+
"8c58e138614bd58742172bd5080d197d2b2dd2f3": "-----BEGIN CERTIFICATE-----\nMIIDJjCCAg6gAwIBAgIIFqFLh91FQ0kwDQYJKoZIhvcNAQEFBQAwNjE0MDIGA1UE\nAxMrZmVkZXJhdGVkLXNpZ25vbi5zeXN0ZW0uZ3NlcnZpY2VhY2NvdW50LmNvbTAe\nFw0xOTA5MjUxNDQ5MzRaFw0xOTEwMTIwMzA0MzRaMDYxNDAyBgNVBAMTK2ZlZGVy\nYXRlZC1zaWdub24uc3lzdGVtLmdzZXJ2aWNlYWNjb3VudC5jb20wggEiMA0GCSqG\nSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDJ/fKZfxfVD68YAuMfl5bnoNBjZ4kyhXMi\nffkizGpJGkMR2gL6ansSYLrd94Gn/W5FH0hMLCWK41gBXXI6alI6y0YNGSeGnmbO\nZz8Si+oMfiPAj9NaawMwNnusdMqrkMUrMBUmWSTzk4ttu3U9TVkIXZ5i0LNvntJO\nuG+Ga4A+CaipE6Y1QoXkFwFDDum8qpXYKMlF0pSbGz/Nb2o1RjINlo9gx+KWgaPK\n2wWw+n6XJDLFtcmhRtQPuMDpxRveY63OlE4CCRhnJLvjSD4ZlxUtqmoDALpRnGUI\n36VdsZpvtz5CsIy8PB+7ZTcBBK8jfy73kxpuuMdlaxZEJuolV8cjAgMBAAGjODA2\nMAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgeAMBYGA1UdJQEB/wQMMAoGCCsG\nAQUFBwMCMA0GCSqGSIb3DQEBBQUAA4IBAQCY7YX8EDcPQGFpklGVtqN7tlfm+Gi8\n0v3E9WoQUiMQJ9Mt34ixd3zPjMeKOtVxj6BZzHrpyR6rM9lB4nzoyCWf9K86HaWS\nuxtAACj7yovKAh2x5pFwEB014qNdG03YYQFy8MvDxegaL4soWqCa2UfEK4vdWyJi\nZKM0iT6s78VY6vOWxK+z1IC/6AYbyskzv57T+dBUwGcwQEf0yBeu89tR7LFSaVV/\n6A+dTGyFlHysR6dddwJvzl7jG83RQs2L58qIISD+6RdRxcD02h388YhhMy9Nrpmo\nZeXouJ7YLsHGFkn3yfWi3KWYVdTGbd/9BQPBjhKzS93SxdolTKKVOI/7\n-----END CERTIFICATE-----\n",
16+
"3db3ed6b9574ee3fcd9f149e59ff0eef4f932153": "-----BEGIN CERTIFICATE-----\nMIIDJjCCAg6gAwIBAgIIeBPD3wqfL6EwDQYJKoZIhvcNAQEFBQAwNjE0MDIGA1UE\nAxMrZmVkZXJhdGVkLXNpZ25vbi5zeXN0ZW0uZ3NlcnZpY2VhY2NvdW50LmNvbTAe\nFw0xOTEwMTExNDQ5MzRaFw0xOTEwMjgwMzA0MzRaMDYxNDAyBgNVBAMTK2ZlZGVy\nYXRlZC1zaWdub24uc3lzdGVtLmdzZXJ2aWNlYWNjb3VudC5jb20wggEiMA0GCSqG\nSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDYpym/gLFOh4IoQhfOeGo+DbUyEIA/0Odf\nmzb9R1nVvM5WFHyqKiT8/yPvLxgXYzYlzyvZu18KAkYWWNuS21Vzhe+d4949P6EZ\n/096QjVFSHvKTo94bSQImeZxZiBhfFcvw/RMM0eTeZZPgOXI3YIJyWjAZ9FUslt7\nWoLU0HZFc/JyPRF8M2kinkdYxnzA+MjzCetXlqmhAr+wLPg/QLKwACyRIF2FJHgf\nPsvqaeF7JXo0zHPcGuHUOqXCHon6KiHZF7OC4bzTuTEzVipJTLYy9QUyL4M2L8bQ\nu1ISUSaXhj+i1WT0RDJwqpioOFprVFqqkVvbUW0nXD/x1UA4nvf7AgMBAAGjODA2\nMAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgeAMBYGA1UdJQEB/wQMMAoGCCsG\nAQUFBwMCMA0GCSqGSIb3DQEBBQUAA4IBAQBr5+4ZvfhP436NdJgN0Jn7iwwVArus\nXUn0hfuBbCoj1DhuRkP9wyLCpOo6cQS0T5bURVZzirsKc5sXP4fNYXqbfLaBpc7n\njTUtTOIqoA4LKPU7/FH6Qt/UfZ4DQIsKaD3087KdY3ePatSn/HTxvT8Ghqy/JGjf\nLXZehQnlyyCRaCMqv1gEOMiY/8LG3d1hLL7CMphnb4ASk0YMKrWkKhIoa6NWU2Rd\nqp01F4iG44ABpea+ymXAGmWBVPnep51kr/wIPIzr9WvNFAAZW2Enk3+kUWNupuz+\npdXq9KnegVsCs4G7QcTPqwc/vMu7uGq/pruDEOYVOd9Rm+rr0wlMgkcf\n-----END CERTIFICATE-----\n",
17+
}
18+
1119

1220
def import_with_3(module_name, path):
1321
import importlib.util
22+
1423
spec = importlib.util.spec_from_file_location(module_name, path)
1524
module = importlib.util.module_from_spec(spec)
1625
spec.loader.exec_module(module)
@@ -19,6 +28,7 @@ def import_with_3(module_name, path):
1928

2029
def import_with_2(module_name, path):
2130
import imp
31+
2232
return imp.load_source(module_name, path)
2333

2434

@@ -28,13 +38,17 @@ def get_assistant(filename):
2838
agent_name = os.path.splitext(filename)[0]
2939

3040
try:
31-
agent_module = import_with_3(
32-
agent_name, os.path.join(os.getcwd(), filename))
41+
agent_module = import_with_3(agent_name, os.path.join(os.getcwd(), filename))
3342

3443
except ImportError:
35-
agent_module = import_with_2(
36-
agent_name, os.path.join(os.getcwd(), filename))
44+
agent_module = import_with_2(agent_name, os.path.join(os.getcwd(), filename))
3745

3846
for name, obj in agent_module.__dict__.items():
3947
if isinstance(obj, Assistant):
4048
return obj
49+
50+
51+
def decode_token(token, client_id):
52+
decoded = jwt.decode(token, certs=GOOGLE_PUBLIC_KEY, verify=True, audience=client_id)
53+
return decoded
54+

requirements.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
11
aniso8601==4.0.1
2+
cachetools==3.1.1
23
certifi==2018.11.29
34
chardet==3.0.4
45
click==7.0
56
flask==1.0.2
7+
google-auth==1.6.3
68
idna==2.8
79
itsdangerous==1.1.0
810
jinja2==2.10
911
markupsafe==1.1.0
12+
pyasn1-modules==0.2.7
13+
pyasn1==0.4.7
1014
requests==2.21.0
15+
rsa==4.0
1116
ruamel.yaml==0.15.81
17+
six==1.12.0
1218
urllib3==1.24.1
1319
werkzeug==0.14.1

samples/account_linking/webhook.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
from flask import Flask
2+
from flask_assistant import Assistant, ask, profile, sign_in
3+
4+
5+
app = Flask(__name__)
6+
7+
app.config['INTEGRATIONS'] = ['ACTIONS_ON_GOOGLE']
8+
app.config['AOG_CLIENT_ID'] = "CLIENT_ID OBTAINED BY SETTING UP ACCOUNT LINKING IN AOG CONSOLE"
9+
10+
11+
assist = Assistant(app=app, route="/", project_id="YOUR_GCP_PROJECT_ID")
12+
13+
@assist.action("Default Welcome Intent")
14+
def welcome():
15+
if profile:
16+
return ask(f"Welcome back {profile['name']}")
17+
18+
return sign_in("To learn more about you")
19+
20+
# this intent must have the actions_intent_SIGN_IN event
21+
# and will be invoked once the user has
22+
@assist.action("Complete-Sign-In")
23+
def complete_sign_in():
24+
if profile:
25+
return ask(f"Welcome aboard {profile['name']}, thanks for signing up!")
26+
else:
27+
return ask("Hope you sign up soon! Would love to get to know you!")
28+
29+
30+
if __name__ == "__main__":
31+
app.run(debug=True)
32+

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
zip_safe=False,
2424
include_package_data=True,
2525
platforms="any",
26-
install_requires=["Flask", "requests", "ruamel.yaml", "aniso8601"],
26+
install_requires=["Flask", "requests", "ruamel.yaml", "aniso8601", "google-auth"],
2727
setup_requires=["pytest-runner"],
2828
tests_require=["pytest"],
2929
test_suite="tests",

0 commit comments

Comments
 (0)