Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Web App - Login w/ Google #89

Open
s2t2 opened this issue Aug 3, 2021 · 0 comments
Open

Web App - Login w/ Google #89

s2t2 opened this issue Aug 3, 2021 · 0 comments

Comments

@s2t2
Copy link
Member

s2t2 commented Aug 3, 2021

Add instructions to the web app exercise, for students looking for further exploration. Snippets below:

Requirements.txt file (uses Authlib package):

# ...

# 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 %}

User profile page:

{% extends "bootstrap_5_layout.html" %}
{% set active_page = "user_profile" %}

{% block content %}

    <h1>User Profile</h1>

    <p>Name: <code>{{ user["name"] }}</code> </p>
    <p>Email: <code>{{ user["email"] }}</code> </p>
    <p>Locale: <code>{{ user["locale"] }}</code> </p>

    <p><a href="/user/logout">Logout</a></p>
{% endblock %}

Navbar with user icon and detection of current logged-in user (goes in twitter bootstrap layout file):

<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
        <div class="container-fluid">
            <a class="navbar-brand" href="/">
                <i class="bi-calendar3" style="font-size: 1.7rem; color: white;"></i>
                &nbsp;
                {{ config.APP_TITLE }}
            </a>

            <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
                <span class="navbar-toggler-icon"></span>
            </button>

            <div class="collapse navbar-collapse" id="navbarSupportedContent">
                <ul class="navbar-nav ms-auto mb-2 mb-lg-0">

                {% if session["current_user"] %}

                    <!-- PROTECTED NAV -->
                    {% for href, page_id, link_text in protected_nav %}
                        {% if page_id == active_page %}
                            {% set is_active = "active" -%}
                        {% else %}
                            {% set is_active = "" -%}
                        {% endif %}
                        <li class="nav-item">
                            <a class="nav-link {{ is_active }}" href="{{href}}">{{link_text}}</a>
                        </li>
                    {% endfor %}

                    <a href="/user/profile" style="padding:5px">
                        <img class="rounded-circle" src="{{ session['current_user']['picture'] }}" alt="profile photo" height="32px" width="32px">
                    </a>

                {% else %}

                    <!-- PUBLIC NAV -->
                    {% for href, page_id, link_text in public_nav %}
                        {% if page_id == active_page %}
                            {% set is_active = "active" -%}
                        {% else %}
                            {% set is_active = "" -%}
                        {% endif %}
                        <li class="nav-item">
                            <a class="nav-link {{ is_active }}" href="{{href}}">{{link_text}}</a>
                        </li>
                    {% endfor %}

                {% endif %}

                </ul>
            </div>
        </div>
    </nav>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

No branches or pull requests

1 participant