# ...
# web app oauth login with google:
Authlib==0.15.3
# ...
Init file (web application factory pattern):
import os
from flask import Flask
from authlib.integrations.flask_client import OAuth
from dotenv import load_dotenv
from app import APP_ENV, APP_VERSION
#from app.firebase_service import FirebaseService
#from app.gcal_service import SCOPES as GCAL_SCOPES #, GoogleCalendarService
from web_app.routes.home_routes import home_routes
from web_app.routes.user_routes import user_routes
from web_app.routes.calendar_routes import calendar_routes
load_dotenv()
GA_TRACKER_ID = os.getenv("GA_TRACKER_ID", default="G-OOPS")
SECRET_KEY = os.getenv("SECRET_KEY", default="super secret") # IMPORTANT: override in production
GOOGLE_OAUTH_CLIENT_ID = os.getenv("GOOGLE_OAUTH_CLIENT_ID")
GOOGLE_OAUTH_CLIENT_SECRET = os.getenv("GOOGLE_OAUTH_CLIENT_SECRET")
def create_app():
app = Flask(__name__)
app.config["APP_ENV"] = APP_ENV
app.config["APP_VERSION"] = APP_VERSION
app.config["APP_TITLE"] = "Our Gym Calendar"
#app.config["GA_TRACKER_ID"] = GA_TRACKER_ID # for client-side google analytics
app.config["SECRET_KEY"] = SECRET_KEY # for flask flash messaging
#app.config["FIREBASE_SERVICE"] = FirebaseService()
#app.config["GCAL_SERVICE"] = GoogleCalendarService()
# GOOGLE LOGIN
oauth = OAuth(app)
oauth_scopes = "openid email profile" + " " + " ".join(GCAL_SCOPES)
print("OAUTH SCOPES:", oauth_scopes)
oauth.register(
name="google",
client_id=GOOGLE_OAUTH_CLIENT_ID,
client_secret=GOOGLE_OAUTH_CLIENT_SECRET,
server_metadata_url="https://accounts.google.com/.well-known/openid-configuration",
client_kwargs={"scope": oauth_scopes},
authorize_params={"access_type": "offline"} # give us the refresh token! see: https://stackoverflow.com/questions/62293888/obtaining-and-storing-refresh-token-using-authlib-with-flask
) # now you can also access via: oauth.google (the name specified during registration)
app.config["OAUTH"] = oauth
app.register_blueprint(home_routes)
app.register_blueprint(user_routes)
app.register_blueprint(calendar_routes)
return app
if __name__ == "__main__":
my_app = create_app()
my_app.run(debug=True)
Authentication wrapper for protected routes (requires google login to view, otherwise will be redirected home):
# web_app/routes/auth.py or something
import functools
from flask import session, flash, redirect
def authenticated_route(view):
"""
Wrap a route with this decorator and assume it will have access to the "current_user"
See: https://flask.palletsprojects.com/en/2.0.x/tutorial/views/#require-authentication-in-other-views
"""
@functools.wraps(view)
def wrapped_view(**kwargs):
if session.get("current_user"):
return view(**kwargs)
else:
print("UNAUTHENTICATED...")
flash("Unauthenticated. Please login!", "warning")
return redirect("/user/login")
return wrapped_view
Login routes:
# web_app/routes/user_routes.py or something
# SEE:
# ... https://docs.authlib.org/en/stable/client/flask.html
# ... https://github.com/authlib/demo-oauth-client/tree/master/flask-google-login
# ... https://github.com/Vuka951/tutorial-code/blob/master/flask-google-oauth2/app.py
# ... https://flask.palletsprojects.com/en/2.0.x/tutorial/views/
from flask import Blueprint, render_template, flash, redirect, current_app, url_for, session #, jsonify
from web_app.routes.auth import authenticated_route
user_routes = Blueprint("user_routes", __name__)
@user_routes.route("/user/login")
@user_routes.route("/user/login/form")
def login_form():
print("LOGIN FORM...")
return render_template("user_login_form.html")
@user_routes.route("/user/login/redirect", methods=["POST"])
def login_redirect():
print("LOGIN REDIRECT...")
oauth = current_app.config["OAUTH"]
redirect_uri = url_for("user_routes.login_callback", _external=True)
return oauth.google.authorize_redirect(redirect_uri)
@user_routes.route("/user/login/callback")
def login_callback():
print("LOGIN CALLBACK...")
oauth = current_app.config["OAUTH"]
# user info (below) needs this to be invoked, even if not directly using the token
# avoids... authlib.integrations.base_client.errors.MissingTokenError: missing_token
token = oauth.google.authorize_access_token()
print("TOKEN:", token["token_type"], token["scope"], token["expires_in"] )
#> {
#> 'access_token': '___.___-___-___-___-___-___',
#> 'expires_at': 1621201708,
#> 'expires_in': 3599,
#> 'id_token': '___.___.___-___-___-___-___',
#> 'refresh_token': "____",
#> 'scope': 'https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email openid',
#> 'token_type': 'Bearer'
#> }
session["bearer_token"] = token
#print(token)
user = oauth.google.userinfo()
user = dict(user)
#print("USER INFO:", type(user)) #> <class 'authlib.oidc.core.claims.UserInfo'>
print(user)
#> {
#> 'email': 'hello@example.com',
#> 'email_verified': True,
#> 'family_name': 'Student',
#> 'given_name': 'Sammy S',
#> 'locale': 'en',
#> 'name': 'Sammy S Student',
#> 'picture': 'https://lh3.googleusercontent.com/a-/___=___-___',
#> 'sub': 'abc123def456789'
#> }
# store user info in the session:
session["current_user"] = user
flash(f"Login success. Welcome, {user['given_name']}!", "success")
return redirect("/user/profile")
@user_routes.route("/user/profile")
@authenticated_route
def profile():
print("PROFILE...")
current_user = session.get("current_user")
return render_template("user_profile.html", user=current_user)
@user_routes.route("/user/logout")
def logout():
print("LOGOUT...")
session.clear() # FYI: this clears the flash as well
#flash("Logout success!", "success")
return redirect("/user/login")
Login form (view)
{% extends "bootstrap_5_layout.html" %}
{% set active_page = "user_login" %}
{% block content %}
<h1>User Login</h1>
<p>To access application functionality, first login with your Google Account...</p>
<form action="/user/login/redirect" method="POST">
<button>Login w/ Google</button>
</form>
{% endblock %}
Add instructions to the web app exercise, for students looking for further exploration. Snippets below:
Requirements.txt file (uses Authlib package):
Init file (web application factory pattern):
Authentication wrapper for protected routes (requires google login to view, otherwise will be redirected home):
Login routes:
Login form (view)
User profile page:
Navbar with user icon and detection of current logged-in user (goes in twitter bootstrap layout file):