singingwolfboy / flask-dance

Doing the OAuth dance with style using Flask, requests, and oauthlib.
https://pypi.python.org/pypi/Flask-Dance/
MIT License
997 stars 156 forks source link

Discord login, error: json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0) #400

Closed pavel-tashev closed 1 year ago

pavel-tashev commented 1 year ago

I implemented successfully social login via Flask-Dance for Google, Facebook, Twitter and Github. I decided to do the same for Discord. Basically the code I used is identical for all social networks. The only differences are:

The code for Discord looks like this (a few snippets):

app.py

import os
from flask import Flask, jsonify, render_template

from appAuth import appAuth
from models import db, login_manager
from oauth import discord_blueprint

app = Flask(__name__)
os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1'
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///./users.db"
app.secret_key = "supersecretkey"

app.register_blueprint(appAuth)
app.register_blueprint(discord_blueprint, url_prefix="/discord_login")

db.init_app(app)
login_manager.init_app(app)
with app.app_context():
    db.create_all()

@app.route("/")
def home():
    return render_template("index.html")

@app.errorhandler(404)
def uri_not_found(e) -> tuple:
    return jsonify([]), 404

if __name__ == "__main__":
    app.run(debug=True)

appAuth.py

from flask import Blueprint, redirect, url_for
from flask_login import login_required, logout_user
from flask_dance.contrib.discord import discord

appAuth = Blueprint("appAuth", __name__)

@appAuth.route("/v1/auth/discord")
def login_discord():
    if not discord.authorized:
        return redirect(url_for("discord.login"))

    res = discord.get("/users/@me")
    username = res.json()["username"]

    return f"You are @{username} on Discord"

@appAuth.route("/logout")
@login_required
def logout():
    logout_user()

    return redirect(url_for("home"))

oauth.py

from sqlalchemy.orm.exc import NoResultFound
from flask_login import current_user, login_user
from flask_dance.consumer import oauth_authorized
from flask_dance.consumer.storage.sqla import SQLAlchemyStorage
from flask_dance.contrib.discord import discord, make_discord_blueprint
from models import db, OAuth, User

discord_blueprint = make_discord_blueprint(
    client_id='<CLIENT_ID>',
    client_secret='<CLIENT_SECRET>,
    scope='identify email',
    redirect_url='http://localhost:5000/discord_login/discord/authorized',
    storage=SQLAlchemyStorage(
        OAuth,
        db.session,
        user=current_user,
        user_required=False,
    ),
)

@oauth_authorized.connect_via(discord_blueprint)
def discord_logged_in(blueprint, token):
    info = discord.get("/users/@me")
    if info.ok:
        account_info = info.json()
        username = account_info["username"]

        query = User.query.filter_by(username=username)
        try:
            user = query.one()
        except NoResultFound:
            user = User(username=username)
            db.session.add(user)
            db.session.commit()
        login_user(user)

models.py

from flask_sqlalchemy import SQLAlchemy
from flask_login import UserMixin, LoginManager
from flask_dance.consumer.storage.sqla import OAuthConsumerMixin

db = SQLAlchemy()

class User(UserMixin, db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(250), unique=True)

class OAuth(OAuthConsumerMixin, db.Model):
    user_id = db.Column(db.Integer, db.ForeignKey(User.id))
    user = db.relationship(User)

login_manager = LoginManager()

@login_manager.user_loader
def load_user(user_id):
    return User.query.get(user_id)

As I mentioned, the code is identical for the rest of the social networks with small adaptations here and there.

The thing is that when I start the process of logging in and approve my login via the Discord interface, discord.get("/users/@me") fires an error:

127.0.0.1 - - [14/Aug/2022 15:52:06] "GET /discord_login/discord/authorized?code=<SOME_CODE_HERE>&state=<STATE>HTTP/1.1" 500 -
Traceback (most recent call last):
  File "/Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/site-packages/flask/app.py", line 2091, in __call__
    return self.wsgi_app(environ, start_response)
  File "/Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/site-packages/flask/app.py", line 2076, in wsgi_app
    response = self.handle_exception(e)
  File "/Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/site-packages/flask/app.py", line 2073, in wsgi_app
    response = self.full_dispatch_request()
  File "/Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/site-packages/flask/app.py", line 1519, in full_dispatch_request
    rv = self.handle_user_exception(e)
  File "/Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/site-packages/flask/app.py", line 1517, in full_dispatch_request
    rv = self.dispatch_request()
  File "/Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/site-packages/flask/app.py", line 1503, in dispatch_request
    return self.ensure_sync(self.view_functions[rule.endpoint])(**req.view_args)
  File "/Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/site-packages/flask_dance/consumer/oauth2.py", line 277, in authorized
    results = oauth_authorized.send(self, token=token) or []
  File "/Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/site-packages/blinker/base.py", line 263, in send
    return [(receiver, receiver(sender, **kwargs))
  File "/Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/site-packages/blinker/base.py", line 263, in <listcomp>
    return [(receiver, receiver(sender, **kwargs))
  File "/Users/admin/Documents/SoftwareDevelopment/Business/CoinLottery/api/oauth.py", line 129, in discord_logged_in
    account_info = info.json()
  File "/Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/site-packages/requests/models.py", line 910, in json
    return complexjson.loads(self.text, **kwargs)
  File "/Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/json/__init__.py", line 346, in loads
    return _default_decoder.decode(s)
  File "/Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/json/decoder.py", line 337, in decode
    obj, end = self.raw_decode(s, idx=_w(s, 0).end())
  File "/Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/json/decoder.py", line 355, in raw_decode
    raise JSONDecodeError("Expecting value", s, err.value) from None

json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)

It seems that the data returned by Discord is not in JSON format.

May I ask for help.

daenney commented 1 year ago

It's probably helpful to start with printing the actual data so we can see what it is.

So instead of doing account_info = info.json(), print info for starters.

pavel-tashev commented 1 year ago

As @daenney requested:

print(info)

returns:

<Response [200]>

And:

print(info.content)

returns the following:

<!DOCTYPE html>\n
            <html>\n  
                <head>
                    <meta charset="utf-8" />\n    
                    <meta content="width=device-width, initial-scale=1.0, maximum-scale=3.0" name="viewport" />\n\n    
                    <!-- section:seometa -->\n    
                    <meta property="og:type" content="website" />\n    
                    <meta property="og:site_name" content="Discord" />\n    
                    <meta property="og:title" content="Discord - A New Way to Chat with Friends & Communities" />\n    
                    <meta\n      property="og:description"\n      content="Discord is the easiest way to communicate over voice, video, and text.  Chat, hang out, and stay close with your friends and communities." />
                    <meta property="og:image" content="undefined//discord.com/assets/652f40427e1f5186ad54836074898279.png" />
                    <meta name="twitter:card" content="summary_large_image" />\n    
                    <meta name="twitter:site" content="@discord" />\n    
                    <meta name="twitter:creator" content="@discord" />\n    
                    <!-- endsection -->
                    <script nonce="MTY0LDAsMTQzLDE5MiwyMTYsNywxNDEsMjA0">window.GLOBAL_ENV = {\n        API_ENDPOINT: \'//discord.com/api\',\n        API_VERSION: 9,\n        GATEWAY_ENDPOINT: \'wss://gateway.discord.gg\',\n        WEBAPP_ENDPOINT: \'//discord.com\',\n        CDN_HOST: \'cdn.discordapp.com\',\n        ASSET_ENDPOINT: \'//discord.com\',\n        MEDIA_PROXY_ENDPOINT: \'//media.discordapp.net\',\n        WIDGET_ENDPOINT: \'//discord.com/widget\',\n        INVITE_HOST: \'discord.gg\',\n        GUILD_TEMPLATE_HOST: \'discord.new\',\n        GIFT_CODE_HOST: \'discord.gift\',\n        RELEASE_CHANNEL: \'stable\',\n        MARKETING_ENDPOINT: \'//discord.com\',\n        BRAINTREE_KEY: \'production_5st77rrc_49pp2rp4phym7387\',\n        STRIPE_KEY: \'pk_live_CUQtlpQUF0vufWpnpUmQvcdi\',\n        NETWORKING_ENDPOINT: \'//router.discordapp.net\',\n        RTC_LATENCY_ENDPOINT: \'//latency.discord.media/rtc\',\n        ACTIVITY_APPLICATION_HOST: \'discordsays.com\',\n        PROJECT_ENV: \'production\',\n        REMOTE_AUTH_ENDPOINT: \'//remote-auth-gateway.discord.gg\',\n        SENTRY_TAGS: {"buildId":"8b6078fde0d0e726607ba343005232983c4ba674","buildType":"normal"},\n        MIGRATION_SOURCE_ORIGIN: \'https://discordapp.com\',\n        MIGRATION_DESTINATION_ORIGIN: \'https://discord.com\',\n        HTML_TIMESTAMP: Date.now(),\n        ALGOLIA_KEY: \'aca0d7082e4e63af5ba5917d5e96bed0\',\n      };</script>
                    <script nonce="MTY0LDAsMTQzLDE5MiwyMTYsNywxNDEsMjA0">!function(){if(null!=window.WebSocket){if(function(n){try{var e=localStorage.getItem(n);return null==e?null:JSON.parse(e)}catch(n){return null}}("token")&&!window.__OVERLAY__){var n=null!=window.DiscordNative||null!=window.require?"etf":"json",e=window.GLOBAL_ENV.GATEWAY_ENDPOINT+"/?encoding="+n+"&v="+window.GLOBAL_ENV.API_VERSION+"&compress=zlib-stream";console.log("[FAST CONNECT] connecting to: "+e);var o=new WebSocket(e);o.binaryType="arraybuffer";var t=Date.now(),i={open:!1,identify:!1,gateway:e,messages:[]};o.onopen=function(){console.log("[FAST CONNECT] connected in "+(Date.now()-t)+"ms"),i.open=!0},o.onclose=o.onerror=function(){window._ws=null},o.onmessage=function(n){i.messages.push(n)},window._ws={ws:o,state:i}}}}();</script>
                    <link rel="prefetch" as="script" href="/assets/d73abb0b449bd4f2a6a7.js"></link>
                    <link rel="prefetch" as="script" href="/assets/4b58fa778cc38e586a72.js"></link>
                    <link rel="prefetch" as="script" href="/assets/b4499d2a6b9046b1b402.js"></link>
                    <link rel="prefetch" as="script" href="/assets/d9d17ccc9fa8d66999f8.js"></link>
                    <link rel="prefetch" as="script" href="/assets/38c96749096ac4dbb7f0.js"></link>
                    <link rel="prefetch" as="script" href="/assets/a20c46bd659294422d41.js"></link>
                    <link rel="prefetch" as="script" href="/assets/89c07f6464c4df65e564.js"></link>
                    <link rel="prefetch" as="script" href="/assets/dc27e9f8c501ecdfe290.js"></link>
                    <link rel="prefetch" as="script" href="/assets/e562e8d483d8ed775251.js"></link>
                    <link rel="prefetch" as="script" href="/assets/deed4f29c12377445043.js"></link>
                    <link rel="prefetch" as="script" href="/assets/3a6a6e151214bd858df7.js"></link>
                    <link rel="prefetch" as="script" href="/assets/2c82ef2768b0855aca3c.js"></link>
                    <link rel="prefetch" as="script" href="/assets/2354c2075ac8d8fcf1c1.js"></link>
                    <link rel="prefetch" as="script" href="/assets/ef497b64ff5f1a43fbc1.js"></link>
                    <link rel="prefetch" as="script" href="/assets/1d87ab524d121c409ad2.js"></link>
                    <link rel="prefetch" as="script" href="/assets/6c3ab714fcdd40d370fa.js"></link>
                    <link rel="prefetch" as="script" href="/assets/7bbea302c5e6ddbbefae.js"></link>
                    <link rel="prefetch" as="script" href="/assets/a093b6b8db2fae28d62a.js"></link>
                    <link rel="prefetch" as="script" href="/assets/afc61162c110cc4afb74.js"></link>
                    <link rel="prefetch" as="script" href="/assets/252c816b8ff9d30996cd.js"></link>
                    <link rel="prefetch" as="script" href="/assets/20dd9cce0ccb5f892cfb.js"></link>
                    <link rel="prefetch" as="script" href="/assets/b5349c8954070523501d.js"></link>
                    <link rel="prefetch" as="script" href="/assets/cfbf4b41af9052aebeb0.js"></link>
                    <link rel="prefetch" as="script" href="/assets/155dd9b3955f2008def3.js"></link>
                    <link rel="prefetch" as="script" href="/assets/76e3d7532a2b2d8569c7.js"></link>
                    <link rel="prefetch" as="script" href="/assets/49558cd8012f53c8d8f6.js"></link>
                    <link rel="prefetch" as="script" href="/assets/f5b95df66988c70a1b0a.js"></link>
                    <link rel="prefetch" as="script" href="/assets/6810ccf0ee8b0cec69ed.js"></link>
                    <link rel="prefetch" as="script" href="/assets/c15bc6e0218a0f67acac.js"></link>
                    <link rel="prefetch" as="script" href="/assets/75f1f042fdbdc6e51e48.js"></link>
                    <link rel="prefetch" as="script" href="/assets/236e2285c87b637a260f.js"></link>
                    <link rel="prefetch" as="script" href="/assets/8ba166d21ba53d9e91de.js"></link>
                    <link rel="prefetch" as="script" href="/assets/adf9e9b020d07e7182f3.js"></link>
                    <link rel="prefetch" as="script" href="/assets/6466c68f93485b326b25.js"></link>
                    <link rel="prefetch" as="script" href="/assets/152c589f6521fb4ad8a2.js"></link>
                    <link rel="prefetch" as="script" href="/assets/e4e832c5c8a0fb524d08.js"></link>
                    <link rel="prefetch" as="script" href="/assets/e888aeb7b219900a0068.js"></link>
                    <link rel="prefetch" as="script" href="/assets/c938ed04a26da9949440.js"></link>
                    <link rel="prefetch" as="script" href="/assets/a3c1d908fcee046f5700.js"></link>
                    <link rel="prefetch" as="script" href="/assets/20f8ba12d3a4a5088d5a.js"></link>
                    <link rel="prefetch" as="script" href="/assets/663c6eecb2d4d00937b7.js"></link>
                    <link rel="prefetch" as="script" href="/assets/dbc698cd063e5bb97893.js"></link>
                    <link rel="prefetch" as="script" href="/assets/0ff678146e2a9c655752.js"></link>
                    <link rel="prefetch" as="script" href="/assets/e639408619a77e1afa6c.js"></link>
                    <link rel="prefetch" as="script" href="/assets/90f2a8531197052f8972.js"></link>
                    <link rel="stylesheet" href="/assets/40532.ccb3502fc312f3ad1aac.css" integrity="sha256-mnpYUQdIW1T8FuMRP4HFg7HkW3kt/sv904P8hpi4CFE= sha512-PR2g6Q6VcbNukRYNVUWQ6XnE0dDGH76H5sKe3W5j26gy+d7ET3+UX0/2cftwLef9WlD6d+vCXpO6ffGtc1fzyA==">
                        <link rel="icon" href="/assets/ec2c34cadd4b5f4594415127380a85e6.ico" />
                        <!-- section:title -->\n    
                        <title>Discord</title>\n    
                        <!-- endsection -->\n  
                        <script async nonce="MTY0LDAsMTQzLDE5MiwyMTYsNywxNDEsMjA0" src=\'/cdn-cgi/bm/cv/669835187/api.js\'></script>
                    </head>\n  
                    <body>\n    
                        <div id="app-mount"></div>
                        <script nonce="MTY0LDAsMTQzLDE5MiwyMTYsNywxNDEsMjA0">window.__OVERLAY__ = /overlay/.test(location.pathname)</script>
                        <script nonce="MTY0LDAsMTQzLDE5MiwyMTYsNywxNDEsMjA0">window.__BILLING_STANDALONE__ = /^\\/billing/.test(location.pathname)</script>
                        <script src="/assets/8cb2a51e44237e74ed2d.js" integrity="sha256-MC9qghNeME6BXkp5LMC1NmEHcL9UUD/FGrbv8n81QWE= sha512-y9DoK5nGThSrWAAx4da3eBsCnwLQgpfnvi37DjfGJ3R0PW/hNqz2z1qDwReEwMo4BXujXFlYMuJVBwVTZ3rbkQ=="></script>
                        <script src="/assets/d73abb0b449bd4f2a6a7.js" integrity="sha256-znKqV1/ra/ZB4oWsOeCeJ8WMqAd2HQNx6gbZdJrj+Tg= sha512-VQzDPCUs5QRPEktO96okBbRWYIccMRC1+IEG66u39n+0hYZNotqxErcWmf8no8z4isF7TOtfCrRRH4xZoqgDXw=="></script>
                        <script src="/assets/45fcdb4b53d858b9aa1a.js" integrity="sha256-tCf7uCXO4FZwgHSKrRZ5u8Il2yuskoineQ2uP7Dgm3A= sha512-Pr+rjyefvyHCOcQqvP4/Cy88DeOCofNBz7+NQVZU1WieknpWQ1Z/rcQYSsvyZOoLHCtia/3Noqmc4JT7iKdRMQ=="></script>
                        <script src="/assets/99bca8736e3709d8d43a.js" integrity="sha256-8YZZzZwvpLAtd8X0UbkRClF0pQIdbT2YYVWrSVH1w3k= sha512-MpGRNvMnbeic00GzHOOOQuz9pP2/5PcmLt5wIaVeoEfYe6jg8bevVgm0LZajmpmtocWV0XNLJfToNMBRXp2LSA=="></script>
                        <script nonce="MTY0LDAsMTQzLDE5MiwyMTYsNywxNDEsMjA0">(function(){window[\'__CF$cv$params\']={r:\'73a923fa2a93bcb8\',m:\'nd1VYKqr8Pq_VBhh2QB2uO65z8ojy_6FlaVdklCMFZE-1660474341-0-ASOLoYZ3lHWEbBBBxrt4ug6gFiVAaNv28g0MdI3Mu19sqvS4xAmiUlieskX+bw/qKEvZ2IOuILNcOh0ThYk7D1CW38Mf4wUrom6arTzreVrjAMu23QZrvI560CQMqwlAGA==\',s:[0xc7de268d51,0xc9a795abfc],}})();</script>
                    </body>\n
                </html>
daenney commented 1 year ago

Well, we definitely don't want random HTML.

I think the problem is you're using discord.get("/users/@me"), but that's on discord.com/api/v10, whereas the base_url of the blueprint is discord.com.

So you'll probably want discord.get("/api/v10/users/@me"), though it's available on the oauth2 endpoint too with discord.get("/api/oauth2/@me") which would also work for non-user things (like a bot).

pavel-tashev commented 1 year ago

I confirm:

discord.get("/api/v10/users/@me")

worked like a charm. Thank you @daenney! ;)