weibeu / Flask-Discord

Discord OAuth2 extension for Flask. An Easier implementation of "Log In With Discord".
https://flask-discord.readthedocs.io/en/latest/
MIT License
182 stars 47 forks source link

jwt.exceptions.DecodeError: Not enough segments #52

Closed YonLiud closed 3 years ago

YonLiud commented 3 years ago
import os, random, string
2   from dotenv import dotenv_values
3   from flask import Flask, redirect, url_for, render_template
4   from flask_discord import DiscordOAuth2Session, requires_authorization, Unauthorized
5   import github_utils, db_utils# local files
6   
7   app = Flask(__name__)
8   
9   app.secret_key = bytes(''.join(random.SystemRandom().choice(string.ascii_uppercase + string.digits) for _ in range(64)), 'utf-8')
10  # OAuth2 must make use of HTTPS in production environment.
11  # os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "true"      # !! Only in development environment.
12  # os.environ["FLASK_ENV"] = "development"
13  
14  config = dotenv_values(".env")
15  
16  app.config["DISCORD_CLIENT_ID"] = config["DISCORD_CLIENT_ID"]
17  app.config["DISCORD_CLIENT_SECRET"] = config["DISCORD_CLIENT_SECRET"]
18  app.config["DISCORD_REDIRECT_URI"] = "https://altab.dev/callback"
19  # app.config["DISCORD_BOT_TOKEN"] = config["DISCORD_BOT_TOKEN"]
20  
21  discord = DiscordOAuth2Session(app)
22  
23  def welcome_user(user):
24  dm_channel = discord.bot_request("/users/@me/channels", "POST", json={"recipient_id": user.id})
25  return discord.bot_request(
26      f"/channels/{dm_channel['id']}/messages", "POST", json={"content": "Thanks for authorizing the app!"}
27  )
28  
29  @app.route("/")
30  def index():
31  return render_template("index.html", repos=github_utils.get_repos_list(), users=db_utils.get_users())
32  
33  @app.route("/login/")
34  def login():
35  return discord.create_session()
36  
37  @app.route("/callback/")
38  def callback():
39  discord.callback()
40  user = discord.fetch_user()
41  welcome_user(user)
42  return redirect(url_for(".me"))
43  
44  
45  @app.errorhandler(Unauthorized)
46  def redirect_unauthorized(e):
47  return redirect(url_for("login"))
48  
49  
50  @app.route("/me/")
51  @requires_authorization
52  def me():
53  user = discord.fetch_user()
54  if not db_utils.user_exists(str(user.id)):
55      db_utils.add_user(str(user.id), str(user.username), str(user.avatar_url))
56  return render_template("welcome.html", pic=user.avatar_url, username=user.username)
57  
Aug 14 12:28:42 caddy-ubuntu-s-1vcpu-1gb-fra1-01 gunicorn[7870]: Traceback (most recent call last):
Aug 14 12:28:42 caddy-ubuntu-s-1vcpu-1gb-fra1-01 gunicorn[7870]:   File "/var/www/Portfolio/env3/lib/python3.6/site-packages/flask/app.py", line 2070, in wsgi_app
Aug 14 12:28:42 caddy-ubuntu-s-1vcpu-1gb-fra1-01 gunicorn[7870]:     response = self.full_dispatch_request()
Aug 14 12:28:42 caddy-ubuntu-s-1vcpu-1gb-fra1-01 gunicorn[7870]:   File "/var/www/Portfolio/env3/lib/python3.6/site-packages/flask/app.py", line 1515, in full_dispatch_request
Aug 14 12:28:42 caddy-ubuntu-s-1vcpu-1gb-fra1-01 gunicorn[7870]:     rv = self.handle_user_exception(e)
Aug 14 12:28:42 caddy-ubuntu-s-1vcpu-1gb-fra1-01 gunicorn[7870]:   File "/var/www/Portfolio/env3/lib/python3.6/site-packages/flask/app.py", line 1513, in full_dispatch_request
Aug 14 12:28:42 caddy-ubuntu-s-1vcpu-1gb-fra1-01 gunicorn[7870]:     rv = self.dispatch_request()
Aug 14 12:28:42 caddy-ubuntu-s-1vcpu-1gb-fra1-01 gunicorn[7870]:   File "/var/www/Portfolio/env3/lib/python3.6/site-packages/flask/app.py", line 1499, in dispatch_request
Aug 14 12:28:42 caddy-ubuntu-s-1vcpu-1gb-fra1-01 gunicorn[7870]:     return self.ensure_sync(self.view_functions[rule.endpoint])(**req.view_args)
Aug 14 12:28:42 caddy-ubuntu-s-1vcpu-1gb-fra1-01 gunicorn[7870]:   File "/var/www/Portfolio/app.py", line 39, in callback
Aug 14 12:28:42 caddy-ubuntu-s-1vcpu-1gb-fra1-01 gunicorn[7870]:     discord.callback()
Aug 14 12:28:42 caddy-ubuntu-s-1vcpu-1gb-fra1-01 gunicorn[7870]:   File "/var/www/Portfolio/Flask-Discord/flask_discord/client.py", line 160, in callback
Aug 14 12:28:42 caddy-ubuntu-s-1vcpu-1gb-fra1-01 gunicorn[7870]:     return jwt.decode(state, current_app.config["SECRET_KEY"], algorithms="HS256")
Aug 14 12:28:42 caddy-ubuntu-s-1vcpu-1gb-fra1-01 gunicorn[7870]:   File "/var/www/Portfolio/env3/lib/python3.6/site-packages/jwt/api_jwt.py", line 119, in decode
Aug 14 12:28:42 caddy-ubuntu-s-1vcpu-1gb-fra1-01 gunicorn[7870]:     decoded = self.decode_complete(jwt, key, algorithms, options, **kwargs)
Aug 14 12:28:42 caddy-ubuntu-s-1vcpu-1gb-fra1-01 gunicorn[7870]:   File "/var/www/Portfolio/env3/lib/python3.6/site-packages/jwt/api_jwt.py", line 95, in decode_complete
Aug 14 12:28:42 caddy-ubuntu-s-1vcpu-1gb-fra1-01 gunicorn[7870]:     **kwargs,
Aug 14 12:28:42 caddy-ubuntu-s-1vcpu-1gb-fra1-01 gunicorn[7870]:   File "/var/www/Portfolio/env3/lib/python3.6/site-packages/jwt/api_jws.py", line 146, in decode_complete
Aug 14 12:28:42 caddy-ubuntu-s-1vcpu-1gb-fra1-01 gunicorn[7870]:     payload, signing_input, header, signature = self._load(jwt)
Aug 14 12:28:42 caddy-ubuntu-s-1vcpu-1gb-fra1-01 gunicorn[7870]:   File "/var/www/Portfolio/env3/lib/python3.6/site-packages/jwt/api_jws.py", line 190, in _load
Aug 14 12:28:42 caddy-ubuntu-s-1vcpu-1gb-fra1-01 gunicorn[7870]:     raise DecodeError("Not enough segments") from err
Aug 14 12:28:42 caddy-ubuntu-s-1vcpu-1gb-fra1-01 gunicorn[7870]: jwt.exceptions.DecodeError: Not enough segments

Server on Production Worked on localhost

There's a reverse proxy by caddy but that's not the issue, I hope at least its not

altab.dev {
        reverse_proxy 0.0.0.0:5000
}

Using a Forked Version of Flask-Discord (Slightly modified version, no big deal, it didn't work when I try the original version aswell)

weibeu commented 3 years ago

Can you confirm these two things in your production deployment:

Also, just in case please do check if you aren't using different domain/subdomain/hostname for your discord create session endpoint and discord callback endpoint.

YonLiud commented 3 years ago

Can you confirm these two things in your production deployment:

  • Flask-Discord (your fork based on) is at latest version.
  • pyjwt is latest version.

Also, just in case please do check if you aren't using different domain/subdomain/hostname for your discord create session endpoint and discord callback endpoint.

everything to its latest, confirmed several times

weibeu commented 3 years ago

Ah, apologies. I missed looking into your code thoroughly. What's going wrong here is the way you're setting up your app secret key. It's being set to some random bytes at runtime. Which allows for possible cases where your application signs some data with say key1 and then at callback, it tries to verify it using key2 and thus raising the jwt DecodeError exception.

It didn't showed up locally because you might be running a single instance of flask application for testing where the secret key stays fixed through all over application lifetime. However, in production gunicorn may create multiple workers where say your discord session is created on worker1 but the callback happens on worker2, both with entirely different secret keys. Which is potential cause of the issue here.

So the suggestion is to have a cryptographiclly random stuff as secret key fixed in environment variable which should not changed at runtime at least. Changing it indirectly means invalidating all of your users session, cookies, etc.

YonLiud commented 3 years ago

Weird, it might be possible but when I tried the same without your module and just the vanilla way of doing it (with requests_oauthlib), it works fine...

weibeu commented 3 years ago

That might work as in your vanilla version you wouldn't be signing and verifying JWTs using your flask secret key.

YonLiud commented 3 years ago

ah that is possible, ima try with a permanent key and see if it will fix! thanks anyway!