Skip to content

Commit ce13fcb

Browse files
committed
Add initial version of Walkthrough 6.
This commit is largely the same as tg/1468549 and tg/1498914 with changes to the attachment_routes.py file and associated HTML templates to provide grade passback functionality. Walkthrough 6 proposal is here: https://docs.google.com/document/d/1Phtn-1cBQL1Ch_YHv-t3AYBjUtchy6sfnebb-0zFHZU/r/0-98dBFqrqY5LUdwz99mxBtw Fixes b/238201287 Change-Id: I61eee37a399b8e4fc94938bc4216cc4844a215ba
1 parent 3a8918f commit ce13fcb

28 files changed

+1955
-0
lines changed

flask/06-grade-sync/config.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
#!/usr/bin/env python3
2+
# Copyright 2021 Google LLC
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
5+
# use this file except in compliance with the License. You may obtain a copy of
6+
# the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
# License for the specific language governing permissions and limitations under
14+
# the License.
15+
"""The Flask server configuration."""
16+
17+
import os
18+
19+
DATABASE_FILE_NAME = os.path.join(
20+
os.path.abspath(os.path.dirname(__file__)), 'data.sqlite')
21+
22+
23+
class Config(object):
24+
# Note: A secret key is included in the sample so that it works.
25+
# If you use this code in your application, replace this with a truly secret
26+
# key. See https://flask.palletsprojects.com/quickstart/#sessions.
27+
SECRET_KEY = os.environ.get(
28+
'SECRET_KEY') or "REPLACE ME - this value is here as a placeholder."
29+
30+
# Configure the flask cookie settings per the iframe security recommendations:
31+
# https://developers.google.com/classroom/eap/add-ons-alpha/iframes#iframe_security_guidelines
32+
SESSION_COOKIE_SECURE = True
33+
SESSION_COOKIE_HTTPONLY = True
34+
SESSION_COOKIE_SAMESITE = "None"
35+
36+
# Point to a database file in the project root.
37+
SQLALCHEMY_DATABASE_URI = f"sqlite:///{DATABASE_FILE_NAME}"
38+
SQLALCHEMY_TRACK_MODIFICATIONS = False

flask/06-grade-sync/main.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
#!/usr/bin/env python3
2+
# Copyright 2021 Google LLC
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
5+
# use this file except in compliance with the License. You may obtain a copy of
6+
# the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
# License for the specific language governing permissions and limitations under
14+
# the License.
15+
"""Entry point for the Flask server.
16+
17+
Loads the webapp module and starts the server. Choose an appropriate launch
18+
method below before running this program.
19+
20+
WARNING: NOT FOR PRODUCTION
21+
----------------------------
22+
This is a sample application for development purposes. You should follow
23+
best practices when securing your production application and in particular
24+
how you securely store and use OAuth tokens.
25+
26+
Note that:
27+
+ Storing tokens in the session is for demonstration purposes. Be sure to store
28+
your tokens securely in your production application.
29+
+ Be careful not to lose a user's refresh token. You will have to ask the user
30+
to re-authorize your add-on to receive a new one.
31+
32+
Review these resources for additional security considerations:
33+
+ Google Identity developer website: https://developers.google.com/identity
34+
+ OAuth 2.0 Security Best Current Practice:
35+
https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics
36+
+ OAuth 2.0 Threat Model and Security Considerations:
37+
https://datatracker.ietf.org/doc/html/rfc6819"""
38+
39+
from webapp import app
40+
import os
41+
42+
if __name__ == "__main__":
43+
# You have several options for running the web server.
44+
45+
### OPTION 1: Unsecured localhost
46+
# When running locally on unsecured HTTP, use this line to disable
47+
# OAuthlib's HTTPs verification.
48+
49+
# Important: When running in production *do not* leave this option enabled.
50+
# os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "1"
51+
52+
# Run the application on a local server, defaults to http://localhost:5000.
53+
# Note: the OAuth flow requires a TLD, *not* an IP address; "localhost" is
54+
# acceptable, but http://127.0.0.1 is not.
55+
# app.run(debug=True)
56+
57+
### OPTION 2: Secure localhost
58+
# Run the application over HTTPs with a locally stored certificate and key.
59+
# Defaults to https://localhost:5000.
60+
app.run(
61+
host="localhost",
62+
ssl_context=("localhost.pem", "localhost-key.pem"),
63+
debug=True)
64+
65+
### OPTION 3: Production- or cloud-ready server
66+
# Start a Gunicorn server, which is appropriate for use in production or a
67+
# cloud deployment.
68+
# server_port = os.environ.get("PORT", "8080")
69+
# app.run(debug=True, port=server_port, host="0.0.0.0")
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
Flask==2.0.2
2+
Flask_SQLAlchemy==2.5.1
3+
Flask_WTF==1.0.0
4+
google_api_python_client==2.51.0
5+
google_auth_oauthlib==0.4.6
6+
protobuf==4.21.2
7+
requests==2.27.1
8+
WTForms==3.0.1
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
#!/usr/bin/env python3
2+
# Copyright 2021 Google LLC
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
5+
# use this file except in compliance with the License. You may obtain a copy of
6+
# the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
# License for the specific language governing permissions and limitations under
14+
# the License.
15+
"""Initialize the webapp module.
16+
17+
Starts the flask server, loads the config, and initializes the database."""
18+
19+
import flask
20+
import config
21+
from flask_sqlalchemy import SQLAlchemy
22+
from os import path
23+
24+
app = flask.Flask(__name__)
25+
app.config.from_object(config.Config)
26+
27+
db = SQLAlchemy(app)
28+
29+
from webapp import attachment_routes, attachment_discovery_routes, models
30+
from webapp import credential_handler as ch
31+
32+
# Initialize the database file if not created.
33+
if not path.exists(config.DATABASE_FILE_NAME):
34+
db.create_all()
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
#!/usr/bin/env python3
2+
# Copyright 2021 Google LLC
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
5+
# use this file except in compliance with the License. You may obtain a copy of
6+
# the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
# License for the specific language governing permissions and limitations under
14+
# the License.
15+
"""Defines all routes for the Flask server."""
16+
17+
from webapp import app
18+
from webapp import credential_handler as ch
19+
20+
import json
21+
import flask
22+
import requests
23+
24+
import google.oauth2.credentials
25+
import googleapiclient.discovery
26+
27+
28+
@app.route("/")
29+
@app.route("/index")
30+
def index():
31+
"""
32+
Render the index page from the "index.html" template. This is meant to act
33+
as a facsimile of a company's home page.
34+
The Add-on Discovery URL should be set to the /classroom-addon route below.
35+
"""
36+
37+
return flask.render_template(
38+
"index.html", message="You've reached the index page.")
39+
40+
41+
@app.route("/classroom-addon")
42+
def classroom_addon():
43+
"""
44+
Checks if a user is signed in. If so, renders the addon discovery page from
45+
the "addon-discovery.html" template. This is meant to be the landing page
46+
when opening the web app in the Classroom add-on iframe.
47+
Otherwise, renders the "authorization.html" template.
48+
"""
49+
50+
# Retrieve the postId, courseId, and addOnToken query parameters.
51+
if flask.request.args.get("postId"):
52+
flask.session["postId"] = flask.request.args.get("postId")
53+
if flask.request.args.get("courseId"):
54+
flask.session["courseId"] = flask.request.args.get("courseId")
55+
if flask.request.args.get("addOnToken"):
56+
flask.session["addOnToken"] = flask.request.args.get("addOnToken")
57+
58+
# Retrieve the login_hint and hd query parameters.
59+
login_hint = flask.request.args.get("login_hint")
60+
hd = flask.request.args.get("hd")
61+
62+
# It's possible that we might return to this route later, in which case the
63+
# parameters will not be passed in. Instead, use the values cached in the session.
64+
65+
# If neither query parameter is available, use the values in the session.
66+
if login_hint is None and hd is None:
67+
login_hint = flask.session.get("login_hint")
68+
hd = flask.session.get("hd")
69+
70+
# If there's no login_hint query parameter, then check for hd.
71+
# Send the user to the sign in page.
72+
elif hd is not None:
73+
flask.session["hd"] = hd
74+
return ch.start_auth_flow("discovery_callback")
75+
76+
# If the login_hint query parameter is available, we'll store it in the session.
77+
else:
78+
flask.session["login_hint"] = login_hint
79+
80+
# Check if we have any stored credentials for this user.
81+
credentials = ch._credential_handler.get_credentials(login_hint)
82+
83+
# Redirect to the authorization page if we received login_hint but don't
84+
# have any stored credentials for this user. We need the refresh token
85+
# specifically.
86+
if credentials is None:
87+
return ch.start_auth_flow("discovery_callback")
88+
89+
return flask.render_template(
90+
"addon-discovery.html",
91+
message="You've reached the addon discovery page.")
92+
93+
94+
@app.route("/test/<request_type>")
95+
def test_api_request(request_type="username"):
96+
"""
97+
Tests an API request, rendering the result in the "show-api-query-result.html" template.
98+
99+
Args:
100+
request_type: The type of API request to test. Currently only "username" is supported.
101+
"""
102+
103+
credentials = ch._credential_handler.get_credentials()
104+
if credentials is None:
105+
return ch.start_auth_flow("discovery_callback")
106+
107+
# Create an API client and make an API request.
108+
fetched_data = ""
109+
110+
if request_type == "username":
111+
user_info_service = googleapiclient.discovery.build(
112+
serviceName="oauth2", version="v2", credentials=credentials)
113+
114+
flask.session["username"] = (
115+
user_info_service.userinfo().get().execute().get("name"))
116+
117+
fetched_data = flask.session.get("username")
118+
119+
# Save credentials in case access token was refreshed.
120+
flask.session["credentials"] = ch.credentials_to_dict(credentials)
121+
ch._credential_handler.save_credentials_to_storage(credentials)
122+
123+
# Render the results of the API call.
124+
return flask.render_template(
125+
"show-api-query-result.html",
126+
data=json.dumps(fetched_data, indent=2),
127+
data_title=request_type)
128+
129+
130+
@app.route("/discovery-callback")
131+
def discovery_callback():
132+
"""
133+
Runs upon return from the OAuth 2.0 authorization server. Fetches and stores
134+
the user's credentials, including the access token, refresh token, and
135+
allowed scopes.
136+
"""
137+
138+
# Specify the state when creating the flow in the callback so that it can
139+
# verified in the authorization server response.
140+
flow = ch.build_flow_instance("discovery_callback", flask.session["state"])
141+
142+
# Use the authorization server's response to fetch the OAuth 2.0 tokens.
143+
authorization_response = flask.request.url
144+
flow.fetch_token(authorization_response=authorization_response)
145+
146+
# Store credentials in the session.
147+
credentials = flow.credentials
148+
flask.session["credentials"] = ch.credentials_to_dict(credentials)
149+
150+
# The flow is complete!
151+
# Add the credentials to our persistent storage.
152+
# We'll extract the "id" value from the credentials to use as a key.
153+
# This is the user's unique Google ID, and will match the login_hint
154+
# query parameter in the future.
155+
156+
# If we've reached this point, and there is already a record in our
157+
# database for this user, they must be obtaining new credentials;
158+
# update the stored credentials.
159+
ch._credential_handler.save_credentials_to_storage(credentials)
160+
161+
return flask.render_template(
162+
"close-me.html", redirect_destination="classroom_addon")
163+
164+
165+
@app.route("/revoke")
166+
def revoke():
167+
"""
168+
Revokes the logged in user's credentials.
169+
"""
170+
171+
if "credentials" not in flask.session:
172+
return flask.render_template(
173+
"addon-discovery.html",
174+
message="You need to authorize before " +
175+
"attempting to revoke credentials.")
176+
177+
credentials = google.oauth2.credentials.Credentials(
178+
**flask.session["credentials"])
179+
180+
revoke = requests.post(
181+
"https://oauth2.googleapis.com/revoke",
182+
params={"token": credentials.token},
183+
headers={"content-type": "application/x-www-form-urlencoded"})
184+
185+
ch._credential_handler.clear_credentials_in_session()
186+
187+
status_code = getattr(revoke, "status_code")
188+
if status_code == 200:
189+
return ch.start_auth_flow("discovery_callback")
190+
else:
191+
return flask.render_template(
192+
"addon-discovery.html",
193+
message="An error occurred during revocation!")

0 commit comments

Comments
 (0)