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 @@ {% else %} - - + +{# #} {% 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