pythonflaskgoogle-signinflask-oauthlib

How to resolve InvalidClientError (oauthlib.oauth2)


I am currently trying to integrate a Google Sign-In feature into my local Flask Web Application with OAuthlib. While trying to run the finished implementation of the Flask Google-Login feature as shown here at RealPython, I ran into this error:

oauthlib.oauth2.rfc6749.errors.InvalidClientError: (invalid_client) Unauthorized

which points to this line of code in my routes.py file:

client.parse_request_body_response(json.dumps(token_response.json()))

I have no clue as to why I'm getting this error, how do I resolve this?

Here are samples from both my app/__init__.py file and routes.py file:

app/__init__.py

import os
from flask import Flask
from config import Development
from flask_caching import Cache
from flask_login import LoginManager
from flask_sqlalchemy import SQLAlchemy
from oauthlib.oauth2 import WebApplicationClient

app = Flask(__name__)
app.config.from_object(Development)

cache = Cache(app)
db = SQLAlchemy(app)
login_manager = LoginManager()
login_manager.init_app(app)

os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1'
GOOGLE_CLIENT_ID = os.getenv("GOOGLE_CLIENT_ID")
client = WebApplicationClient(GOOGLE_CLIENT_ID)

from app import routes, forms, login, models

app/routes.py

import os

...

GOOGLE_CLIENT_ID = os.getenv("GOOGLE_CLIENT_ID")
GOOGLE_CLIENT_SECRET = os.getenv("GOOGLE_CLIENT_SECRET")
GOOGLE_DISCOVERY_URL = (
    "https://accounts.google.com/.well-known/openid-configuration"
)

def get_google_provider_cfg():
    return requests.get(GOOGLE_DISCOVERY_URL).json()

...


@app.route("/login-google")
def login_google():
    google_provider_cfg = get_google_provider_cfg()
    authorization_endpoint = google_provider_cfg["authorization_endpoint"]

    request_uri = client.prepare_request_uri(
        authorization_endpoint,
        redirect_uri=flask.request.base_url + '/callback',
        scope=["openid", "email", "profile"],
    )
    return flask.redirect(request_uri)

@app.route("/login-google/callback")
def callback():
    code = flask.request.args.get("code")
    google_provider_cfg = get_google_provider_cfg()
    token_endpoint = google_provider_cfg["token_endpoint"]
    token_url, headers, body = client.prepare_token_request(
        token_endpoint, authorization_response=flask.request.url,
        redirect_url=flask.request.base_url, code=code
    )
    token_response = requests.post(
        token_url, headers=headers,
        data=body, auth=(GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET),
    )
    client.parse_request_body_response(json.dumps(token_response.json())) ##  <--- Error Occurs Here!
    userinfo_endpoint = google_provider_cfg["userinfo_endpoint"]
    uri, headers, body = client.add_token(userinfo_endpoint)
    userinfo_response = requests.get(uri, headers=headers, data=body)
    flags = userinfo_response.json().get("email_verified")

    
...


Solution

  • You mean you want to make google - login to your flask website? I have my own code, it work well First, init.py

    from flask import Flask
    from flask_sqlalchemy import SQLAlchemy
    from flask_login import LoginManager
    import dir
    from flask_mail import Mail
    
    # init SQLAlchemy so we can use it later in our models
    db = SQLAlchemy()
    mail = Mail()
    
    
    GOOGLE_LOGIN_CLIENT_ID = "xxxxxxxx-xxxxxxxxx.apps.googleusercontent.com"
    GOOGLE_LOGIN_CLIENT_SECRET = "xxxxxxxxx"
    def create_app():
        app = Flask(__name__)
        app.debug = False
        app.config['SECRET_KEY'] = 'secret-key-goes-here'
    
        # app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///db.sqlite'
        app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////' + str(dir.dir) + '/admin.sqlite' #this is dabase, you can change it.
        app.config['MAIL_SERVER'] = 'smtp.gmail.com'
        app.config['MAIL_PORT'] = 465
        app.config['MAIL_USERNAME'] = 'youremail@gmail.com'
        app.config['MAIL_PASSWORD'] = 'yourpass'
        app.config['MAIL_USE_TLS'] = False
        app.config['MAIL_USE_SSL'] = True
        app.config['OAUTH_CREDENTIALS']  = {
            'google': {
                'id': GOOGLE_LOGIN_CLIENT_ID,
                'secret': GOOGLE_LOGIN_CLIENT_SECRET
            }
        }
        mail.init_app(app)
        db.init_app(app)
        with app.app_context():
            db.create_all()
    
        login_manager = LoginManager()
        login_manager.login_view = 'auth.login'
        login_manager.init_app(app)
        from .models import User
        @login_manager.user_loader
        def load_user(user_id):
            # since the user_id is just the primary key of our user table, use it in the query for the user
            return User.query.get(int(user_id))
    
    
    
        # blueprint for auth routes in our app
        from .auth import auth as auth_blueprint
        app.register_blueprint(auth_blueprint)
    
        from .serv import serv as serv_blueprint
        app.register_blueprint(serv_blueprint)
    
        # blueprint for non-auth parts of app
        from .main import main as main_blueprint
        app.register_blueprint(main_blueprint)
    
        from .search import search as sear_blueprint
        app.register_blueprint(sear_blueprint)
    
        from .dash import dash as dash_blueprint
        app.register_blueprint(dash_blueprint)
    
        from .dlf import dlf as dlf_blueprint
        app.register_blueprint(dlf_blueprint)
    
        from .contact import contact as contact_blueprint
        app.register_blueprint(contact_blueprint)
    
    
        return app
    
    
    

    Second, in main.py, you can change to app.py, for easy code in future, i think you should user Blueprint

    from __future__ import print_function
    from flask import Blueprint, render_template, request, redirect, flash, url_for, current_app
    from flask_login import login_required, current_user, login_user
    from project.function import cmsHaravan as hara, cmsCalendar as cal
    from .function import config as cf, cmsContacts as ct
    from datetime import datetime, timedelta, date
    from .models import User, Orders, Shop, Services
    from . import db, mail
    from flask_mail import Message
    import random
    import string
    from werkzeug.security import generate_password_hash
    import json
    from requests_oauthlib import OAuth2Session
    from urllib.request import urlopen
    import dateutil
    from oauth2client.client import OAuth2WebServerFlow
    main = Blueprint('main', __name__)
    class OAuthSignIn(object):
        providers = None
    
        def __init__(self, provider_name):
            self.provider_name = provider_name
            credentials = current_app.config['OAUTH_CREDENTIALS'][provider_name]
            self.consumer_id = credentials['id']
            self.consumer_secret = credentials['secret']
    
        def authorize(self):
            pass
    
        def callback(self):
            pass
    
        def get_callback_url(self):
            return url_for('main.oauth_callback', provider=self.provider_name,
                           _external=True)
    
        @classmethod
        def get_provider(self, provider_name):
            if self.providers is None:
                self.providers = {}
                for provider_class in self.__subclasses__():
                    provider = provider_class()
                    self.providers[provider.provider_name] = provider
            return self.providers[provider_name]
    
    
    class GoogleSignIn(OAuthSignIn):
        openid_url = "https://accounts.google.com/.well-known/openid-configuration"
    
        def __init__(self):
            super(GoogleSignIn, self).__init__("google")
            self.openid_config = json.load(urlopen(self.openid_url))
            self.session = OAuth2Session(
                client_id=self.consumer_id,
                redirect_uri=self.get_callback_url(),
                scope='https://www.googleapis.com/auth/userinfo.profile openid https://www.googleapis.com/auth/userinfo.email '
            )
    
        def authorize(self):
            auth_url, _ = self.session.authorization_url(
                self.openid_config["authorization_endpoint"])
            return redirect(auth_url)
    
        def callback(self):
            if "code" not in request.args:
                return None, None
    
            self.session.fetch_token(
                token_url=self.openid_config["token_endpoint"],
                code=request.args["code"],
                client_secret=self.consumer_secret,
            )
    
            me = self.session.get(self.openid_config["userinfo_endpoint"]).json()
            print(me)
            return me["family_name"], me["given_name"], me["email"], me["picture"]
    
    
    @main.route('/authorize/<provider>')
    def oauth_authorize(provider):
        # Flask-Login function
        if not current_user.is_anonymous:
            return redirect(url_for('main.index'))
        oauth = OAuthSignIn.get_provider(provider)
        return oauth.authorize()
    
    
    @main.route('/callback/<provider>')
    def oauth_callback(provider):
        if not current_user.is_anonymous:
            return redirect(url_for('main.index'))
        oauth = OAuthSignIn.get_provider(provider)
        flamily_name, given_name, email, avatar = oauth.callback()
        print("Successfull to get email", email)
        if email is None:
            # I need a valid email address for my user identification
            flash('Invalid')
            return redirect(url_for('auth.login'))
        # Look if the user already exists
        user = User.query.filter_by(email=email).first()
        if not user:
            # Create the user. Try and use their name returned by Google,
            # but if it is not set, split the email address at the @.
            name = given_name
            if name is None or name == "":
                name = email.split('@')[0]
    
            # We can do more work here to ensure a unique nickname, if you
            # require that.
            user = User(firstname=given_name, lastname=name, email=email, avatar=avatar)
            db.session.add(user)
            db.session.commit()
        # Log in the user, by default remembering them for their next visit
        # unless they log out.
        user.firstname = flamily_name
        user.lastname = given_name
        user.avatar = avatar
        db.session.commit()
        login_user(user, remember=True)
        return redirect(url_for('main.profile'))
    

    Third, in html

    <div class="row">
                    <div class="col "><span class="label-input-group"><br></span>
                        <a href="{{ url_for('main.oauth_authorize', provider='google') }}" class="btn btn-light btn-lg btn-block border-primary text-primary">
                            Login with google with out password
                        </a>
                    </div>
    
    
                </div>
    

    For my code, you have to make the model in models.py

    from flask_login import UserMixin
    from . import db
    from datetime import datetime, timedelta, date
    class User(UserMixin, db.Model):
        __tablename__ = 'user'
        id = db.Column(db.Integer, primary_key=True)  # primary keys are required by SQLAlchemy
        firstname = db.Column(db.String(30))
        lastname = db.Column(db.String(30))
        email = db.Column(db.String(100), unique=True)
        phone = db.Column(db.String(10))
        password = db.Column(db.String(20))
    

    To use model, i use the Flask-Login