diff --git a/.gitignore b/.gitignore
index 373e200e..ea8a4c34 100644
--- a/.gitignore
+++ b/.gitignore
@@ -175,3 +175,6 @@ app/ch13-validation/starter/.idea/inspectionProfiles/Project_Default.xml
app/ch14_testing/final/.idea/inspectionProfiles/Project_Default.xml
app/ch14_testing/starter/.idea/inspectionProfiles/Project_Default.xml
app/ch15_deploy/final/.idea/inspectionProfiles/Project_Default.xml
+app/ch15_deploy/final/pypi_org/flask_session
+d43be29f-e2f2-41e5-8dde-ed049a1776b6.xml
+dataSources.local.xml
diff --git a/app/ch15_deploy/final/.idea/codeStyles/codeStyleConfig.xml b/app/ch15_deploy/final/.idea/codeStyles/codeStyleConfig.xml
new file mode 100644
index 00000000..a55e7a17
--- /dev/null
+++ b/app/ch15_deploy/final/.idea/codeStyles/codeStyleConfig.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/ch15_deploy/final/.idea/dataSources.xml b/app/ch15_deploy/final/.idea/dataSources.xml
new file mode 100644
index 00000000..b9533375
--- /dev/null
+++ b/app/ch15_deploy/final/.idea/dataSources.xml
@@ -0,0 +1,22 @@
+
+
+
+
+ sqlite.xerial
+ true
+ org.sqlite.JDBC
+ jdbc:sqlite:$PROJECT_DIR$/pypi_org/db/pypi.sqlite
+
+
+ file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.25.1/sqlite-jdbc-3.25.1.jar
+
+
+ file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.20.1.1/xerial-sqlite-license.txt
+
+
+ file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.20.1.1/sqlite-jdbc-3.20.1.1.jar
+
+
+
+
+
\ No newline at end of file
diff --git a/app/ch15_deploy/final/.idea/dictionaries/mkennedy.xml b/app/ch15_deploy/final/.idea/dictionaries/mkennedy.xml
new file mode 100644
index 00000000..96aa2a19
--- /dev/null
+++ b/app/ch15_deploy/final/.idea/dictionaries/mkennedy.xml
@@ -0,0 +1,10 @@
+
+
+
+ dateutil
+ passlib
+ tablename
+ toggler
+
+
+
\ No newline at end of file
diff --git a/app/ch15_deploy/final/.idea/flask-deploy.iml b/app/ch15_deploy/final/.idea/flask-deploy.iml
index d0dc6825..1ac5a163 100644
--- a/app/ch15_deploy/final/.idea/flask-deploy.iml
+++ b/app/ch15_deploy/final/.idea/flask-deploy.iml
@@ -2,7 +2,7 @@
-
+
@@ -14,7 +14,4 @@
-
-
-
\ No newline at end of file
diff --git a/app/ch15_deploy/final/.idea/misc.xml b/app/ch15_deploy/final/.idea/misc.xml
index 349d87f1..2c526bdf 100644
--- a/app/ch15_deploy/final/.idea/misc.xml
+++ b/app/ch15_deploy/final/.idea/misc.xml
@@ -3,5 +3,5 @@
-
+
\ No newline at end of file
diff --git a/app/ch15_deploy/final/pypi_org/app.py b/app/ch15_deploy/final/pypi_org/app.py
index d1de87c6..f61a3ea5 100644
--- a/app/ch15_deploy/final/pypi_org/app.py
+++ b/app/ch15_deploy/final/pypi_org/app.py
@@ -2,17 +2,21 @@
import sys
import flask
+from flask_session import Session
+
+from pypi_org.configs import app_config
folder = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
sys.path.insert(0, folder)
import pypi_org.data.db_session as db_session
app = flask.Flask(__name__)
+app.config.from_object(app_config)
def main():
configure()
- app.run(debug=True, port=5006)
+ app.run(debug=True, port=5006, host='localhost')
def configure():
@@ -23,9 +27,15 @@ def configure():
setup_db()
print("DB setup completed.")
+
+ setup_session_server()
print("", flush=True)
+def setup_session_server():
+ Session(app)
+
+
def setup_db():
db_file = os.path.join(
os.path.dirname(__file__),
diff --git a/app/ch15_deploy/final/pypi_org/configs/app_config.py b/app/ch15_deploy/final/pypi_org/configs/app_config.py
new file mode 100644
index 00000000..a8199ea4
--- /dev/null
+++ b/app/ch15_deploy/final/pypi_org/configs/app_config.py
@@ -0,0 +1,30 @@
+b2c_tenant = "talkpythondemos"
+signupsignin_user_flow = "B2C_1_susi"
+editprofile_user_flow = "B2C_1_profileediting1"
+resetpassword_user_flow = "B2C_1_passwordreset1"
+authority_template = "https://{tenant}.b2clogin.com/{tenant}.onmicrosoft.com/{user_flow}"
+
+CLIENT_ID = "115c67cd-3558-4bc4-9180-51c6d2d4fa45" # Application (client) ID of app registration
+
+CLIENT_SECRET = "s.4i3ff~139.58~3DMuE42KvZ8-X4ytAS9" # Placeholder - for use ONLY during testing.
+# In a production app, we recommend you use a more secure method of storing your secret,
+# like Azure Key Vault. Or, use an environment variable as described in Flask's documentation:
+# https://flask.palletsprojects.com/en/1.1.x/config/#configuring-from-environment-variables
+# CLIENT_SECRET = os.getenv("CLIENT_SECRET")
+# if not CLIENT_SECRET:
+# raise ValueError("Need to define CLIENT_SECRET environment variable")
+
+AUTHORITY = authority_template.format(
+ tenant=b2c_tenant, user_flow=signupsignin_user_flow)
+
+REDIRECT_PATH = "/account/auth" # Used for forming an absolute URL to your redirect URI.
+# The absolute URL must match the redirect URI you set
+# in the app's registration in the Azure portal.
+
+# This is the API resource endpoint
+ENDPOINT = '' # Application ID URI of app registration in Azure portal
+
+# These are the scopes you've exposed in the web API app registration in the Azure portal
+SCOPE = [] # Example with two exposed scopes: ["demo.read", "demo.write"]
+
+SESSION_TYPE = "filesystem" # Specifies the token cache should be stored in server-side session
diff --git a/app/ch15_deploy/final/pypi_org/infrastructure/msal_builder.py b/app/ch15_deploy/final/pypi_org/infrastructure/msal_builder.py
new file mode 100644
index 00000000..2a60f5fd
--- /dev/null
+++ b/app/ch15_deploy/final/pypi_org/infrastructure/msal_builder.py
@@ -0,0 +1,18 @@
+import uuid
+
+import msal
+
+from pypi_org.configs import app_config
+
+
+def build_auth_url(authority=None, scopes=None, state=None):
+ return build_msal_app(authority=authority).get_authorization_request_url(
+ scopes or [],
+ state=state or str(uuid.uuid4()),
+ redirect_uri="http://localhost:5006/account/auth")
+
+
+def build_msal_app(cache=None, authority=None):
+ return msal.ConfidentialClientApplication(
+ app_config.CLIENT_ID, authority=authority or app_config.AUTHORITY,
+ client_credential=app_config.CLIENT_SECRET, token_cache=cache)
diff --git a/app/ch15_deploy/final/pypi_org/infrastructure/session_cache.py b/app/ch15_deploy/final/pypi_org/infrastructure/session_cache.py
new file mode 100644
index 00000000..05c4122e
--- /dev/null
+++ b/app/ch15_deploy/final/pypi_org/infrastructure/session_cache.py
@@ -0,0 +1,14 @@
+import msal
+from flask import session
+
+
+def load_cache():
+ cache = msal.SerializableTokenCache()
+ if session.get("token_cache"):
+ cache.deserialize(session["token_cache"])
+ return cache
+
+
+def save_cache(cache):
+ if cache.has_state_changed:
+ session["token_cache"] = cache.serialize()
diff --git a/app/ch15_deploy/final/pypi_org/services/user_service.py b/app/ch15_deploy/final/pypi_org/services/user_service.py
index 4126adce..8f187234 100644
--- a/app/ch15_deploy/final/pypi_org/services/user_service.py
+++ b/app/ch15_deploy/final/pypi_org/services/user_service.py
@@ -41,7 +41,7 @@ def create_user(name: str, email: str, password: str) -> Optional[User]:
def hash_text(text: str) -> str:
- hashed_text = crypto.encrypt(text, rounds=171204)
+ hashed_text = crypto.encrypt(text, rounds=171_204)
return hashed_text
diff --git a/app/ch15_deploy/final/pypi_org/templates/shared/_layout.html b/app/ch15_deploy/final/pypi_org/templates/shared/_layout.html
index b99b6efa..38d0526b 100644
--- a/app/ch15_deploy/final/pypi_org/templates/shared/_layout.html
+++ b/app/ch15_deploy/final/pypi_org/templates/shared/_layout.html
@@ -40,8 +40,8 @@
Account
Logout
{% else %}
- Login
- Register
+ Login or Register
+{# Register#}
{% endif %}
diff --git a/app/ch15_deploy/final/pypi_org/viewmodels/shared/viewmodelbase.py b/app/ch15_deploy/final/pypi_org/viewmodels/shared/viewmodelbase.py
index e1e4d9e0..53abb68d 100644
--- a/app/ch15_deploy/final/pypi_org/viewmodels/shared/viewmodelbase.py
+++ b/app/ch15_deploy/final/pypi_org/viewmodels/shared/viewmodelbase.py
@@ -1,8 +1,11 @@
+import uuid
from typing import Optional
import flask
+import msal
from flask import Request
+from pypi_org.configs import app_config
from pypi_org.infrastructure import request_dict, cookie_auth
@@ -16,3 +19,6 @@ def __init__(self):
def to_dict(self):
return self.__dict__
+
+
+
diff --git a/app/ch15_deploy/final/pypi_org/views/account_views.py b/app/ch15_deploy/final/pypi_org/views/account_views.py
index 8697cc24..47ebea24 100644
--- a/app/ch15_deploy/final/pypi_org/views/account_views.py
+++ b/app/ch15_deploy/final/pypi_org/views/account_views.py
@@ -1,15 +1,69 @@
+import uuid
+
import flask
+from flask import session
+import pypi_org.infrastructure.cookie_auth as cookie_auth
+from pypi_org.configs import app_config
+from pypi_org.infrastructure import session_cache, msal_builder
from pypi_org.infrastructure.view_modifiers import response
from pypi_org.services import user_service
-import pypi_org.infrastructure.cookie_auth as cookie_auth
from pypi_org.viewmodels.account.index_viewmodel import IndexViewModel
from pypi_org.viewmodels.account.login_viewmodel import LoginViewModel
from pypi_org.viewmodels.account.register_viewmodel import RegisterViewModel
+from pypi_org.viewmodels.shared.viewmodelbase import ViewModelBase
blueprint = flask.Blueprint('account', __name__, template_folder='templates')
+# ################### AZURE AUTH ############################
+
+
+@blueprint.route('/account/auth')
+def auth():
+ args = flask.request.args
+
+ if flask.request.args.get('state') != session.get("state"):
+ return flask.redirect('/') # No-OP. Goes back to Index page
+ if "error" in flask.request.args: # Authentication/Authorization failure
+
+ return f"There was an error logging in: Error: {args.get('error')}, details: {args.get('error_description')}."
+ if flask.request.args.get('code'):
+ cache = session_cache.load_cache()
+ result = msal_builder.build_msal_app(cache=cache).acquire_token_by_authorization_code(
+ flask.request.args['code'],
+ scopes=app_config.SCOPE, # Misspelled scope would cause an HTTP 400 error here
+ redirect_uri='http://localhost:5006/account/auth')
+ if "error" in result:
+ return f"There was an error logging in: Error: {args.get('error')}, details: {args.get('error_description')}."
+
+ session_cache.save_cache(cache)
+ # 'oid': '257af28c-d791-4287-bf95-b67578dae57e',
+ claims = result['id_token_claims']
+
+ email = claims.get('emails', ['NONE'])[0].strip().lower()
+ first_name = claims.get('given_name')
+ last_name = claims.get('family_name')
+
+ user = user_service.find_user_by_email(email)
+ if not user:
+ user = user_service.create_user(f'{first_name} {last_name}', email, str(uuid.uuid4()))
+
+ resp = flask.redirect('/account')
+ cookie_auth.set_auth(resp, user.id)
+ return resp
+
+ return flask.redirect('/')
+
+
+@blueprint.route('/account/begin_auth')
+def begin_auth():
+ state = str(uuid.uuid4())
+ session["state"] = state
+
+ return flask.redirect(msal_builder.build_auth_url(state=state))
+
+
# ################### INDEX #################################
@@ -85,7 +139,9 @@ def login_post():
@blueprint.route('/account/logout')
def logout():
- resp = flask.redirect('/')
+ resp = flask.redirect( # Also logout from your tenant's web session
+ app_config.AUTHORITY + "/oauth2/v2.0/logout" +
+ "?post_logout_redirect_uri=http://localhost:5006/")
cookie_auth.logout(resp)
-
+ session.clear() # Wipe out user and its token cache from session
return resp
diff --git a/app/ch15_deploy/final/requirements.txt b/app/ch15_deploy/final/requirements.txt
index e8ba36b0..80461607 100644
--- a/app/ch15_deploy/final/requirements.txt
+++ b/app/ch15_deploy/final/requirements.txt
@@ -1,6 +1,8 @@
flask
+flask-session
werkzeug
sqlalchemy
+msal
progressbar2
python-dateutil