I am using Flask-dance in order to authenticate users to my app. The auth provider is Google.
From time to time the following exception is raised:
2019-08-09 08:07:26 default[20190809t105407] "GET / HTTP/1.1" 500
2019-08-09 08:07:26 default[20190809t105407] -- 0 --
2019-08-09 08:07:26 default[20190809t105407] -- 1 --
2019-08-09 08:07:27 default[20190809t105407] InvalidClientIdError was caught: (invalid_request) Missing required parameter: refresh_token
Looking around this problem I could find 2 directions to go with:
Use offline=True while creating the Google blueprint
Implementing error handler for TokenExpiredError
I did both and deployed my app to GAE but I still face the same error. From the stacktrace I understand that the error handler is being invoked but while the code tries to recover the 'refresh_token' is raised
My code: (The code is based on Google QuickStart and Flask-dance issue 143)
import oauthlib
from flask import Flask, redirect, url_for, flash, session, current_app
from flask_dance.contrib.google import make_google_blueprint, google
import os
import time
from oauthlib.oauth2.rfc6749.errors import InvalidClientIdError, TokenExpiredError
GOOGLE_CLIENT_ID = os.environ.get("GOOGLE_CLIENT_ID", None)
GOOGLE_CLIENT_SECRET = os.environ.get("GOOGLE_CLIENT_SECRET", None)
app = Flask(__name__)
app.secret_key = "TODO_TODO"
blueprint = make_google_blueprint(
client_id=GOOGLE_CLIENT_ID,
client_secret=GOOGLE_CLIENT_SECRET,
offline=True
)
app.register_blueprint(blueprint, url_prefix="/login")
#app.route('/logout', methods=['GET'])
def logout():
_revoke_token_and_empty_session()
return redirect(url_for('app.index'))
def _revoke_token_and_empty_session():
print('inside _revoke_token_and_empty_session')
if google.authorized:
try:
google.get(
'https://accounts.google.com/o/oauth2/revoke',
params={
'token':
current_app.blueprints['google'].token['access_token']},
)
except TokenExpiredError:
pass
except InvalidClientIdError:
# Our OAuth session apparently expired. We could renew the token
# and logout again but that seems a bit silly, so for now fake
# it.
pass
session.clear()
#app.errorhandler(oauthlib.oauth2.rfc6749.errors.TokenExpiredError)
def token_expired(_):
print('In TokenExpiredError')
del blueprint.token
_revoke_token_and_empty_session()
flash('Your session had expired. Please submit the request again',
'error')
return redirect(url_for('app.index'))
#app.route("/")
def index():
print('-- 0 --')
if not google.authorized:
return redirect(url_for("google.login"))
print('-- 1 --')
user_info_url = 'https://openidconnect.googleapis.com/v1/userinfo'
try:
resp = google.get(user_info_url)
except InvalidClientIdError as e:
#
# Here is the problem
#
print('InvalidClientIdError was caught: {}'.format(str(e)))
return 'Having an InvalidClientIdError issue: {}'.format(str(e)), 500
else:
print('-- 2 --')
user_info = resp.json()
return "You are {user_name} on Google. Time: {t}".format(user_name=user_info['name'], t=time.time())
if __name__ == "__main__":
app.run()
My current understanding is that the TokenExpiredError was caught and the function index was called. When the function tries to call resp = google.get(user_info_url) the InvalidClientIdError: (invalid_request) Missing required parameter: refresh_token is raised.
Any idea how to solve it?
From your code, and flask code, looks like you are trying to delete the token before revoking it, even though it's already invalid. If you want to avoid passing "offline=True" then try to simplify the code since there is no need to flash anything (the user will be redirected), and the token is already invalid, so it doesn't make much sense to revoke it either. I'm also setting only one error handler.
Here is the code that worked for me:
import oauthlib
from flask import Flask, redirect, url_for, flash, session, current_app
from flask_dance.contrib.google import make_google_blueprint, google
import os
import time
from oauthlib.oauth2.rfc6749.errors import InvalidClientIdError, TokenExpiredError
GOOGLE_CLIENT_ID = os.environ.get("GOOGLE_CLIENT_ID", None)
GOOGLE_CLIENT_SECRET = os.environ.get("GOOGLE_CLIENT_SECRET", None)
app = Flask(__name__)
app.secret_key = "TODO_TODO"
blueprint = make_google_blueprint(
client_id=GOOGLE_CLIENT_ID,
client_secret=GOOGLE_CLIENT_SECRET,
offline=True
)
app.register_blueprint(blueprint, url_prefix="/login")
#app.route('/logout', methods=['GET'])
def logout():
"""
This endpoint tries to revoke the token
and then it clears the session
"""
if google.authorized:
try:
google.get(
'https://accounts.google.com/o/oauth2/revoke',
params={
'token':
current_app.blueprints['google'].token['access_token']},
)
except TokenExpiredError:
pass
except InvalidClientIdError:
# Our OAuth session apparently expired. We could renew the token
# and logout again but that seems a bit silly, so for now fake
# it.
pass
_empty_session()
return redirect(url_for('app.index'))
def _empty_session():
"""
Deletes the google token and clears the session
"""
if 'google' in current_app.blueprints and hasattr(current_app.blueprints['google'], 'token'):
del current_app.blueprints['google'].token
session.clear()
#app.errorhandler(oauthlib.oauth2.rfc6749.errors.TokenExpiredError)
#app.errorhandler(oauthlib.oauth2.rfc6749.errors.InvalidClientIdError)
def token_expired(_):
_empty_session()
return redirect(url_for('app.index'))
#app.route("/")
def index():
print('-- 0 --')
if not google.authorized:
return redirect(url_for("google.login"))
print('-- 1 --')
user_info_url = 'https://openidconnect.googleapis.com/v1/userinfo'
resp = google.get(user_info_url)
user_info = resp.json()
return "You are {user_name} on Google. Time: {t}".format(user_name=user_info['name'], t=time.time())
if __name__ == "__main__":
app.run()
Related
I'm attempting to set up the Azure AD authentication example from here:
https://github.com/Azure-Samples/ms-identity-python-webapp
I have all configurations set, and permissions on AzureAD, but I'm getting intermittent problems with being able to log in.
I have chased the problem to the session keys either not writing or reading correctly within session.get('user'):
def index():
if not session.get("user"):
return redirect(url_for("login"))
return render_template('index.html', user=session["user"], version=msal.__version__)
additionally,
if request.args.get('state') != session.get("state"):
return redirect(url_for("index")) # No-OP. Goes back to Index page
never returns true, but if I comment it out then sometimes all will work, it will log in, and allow access to the graph app.
I've set the flask_session directory locally, and it is creating caches (around 8kb) for each login.
What seems to help is clearing out the flask_session folder and restarting the application, but not reliably.
Any help would be most gratefully recieved!
full example copied here for convenience:
import uuid
import requests
from flask import Flask, render_template, session, request, redirect, url_for
from flask_session import Session # https://pythonhosted.org/Flask-Session
import msal
import app_config
app = Flask(__name__)
app.config.from_object(app_config)
Session(app)
# This section is needed for url_for("foo", _external=True) to automatically
# generate http scheme when this sample is running on localhost,
# and to generate https scheme when it is deployed behind reversed proxy.
# See also https://flask.palletsprojects.com/en/1.0.x/deploying/wsgi-standalone/#proxy-setups
from werkzeug.middleware.proxy_fix import ProxyFix
app.wsgi_app = ProxyFix(app.wsgi_app, x_proto=1, x_host=1)
#app.route("/")
def index():
if not session.get("user"):
return redirect(url_for("login"))
return render_template('index.html', user=session["user"], version=msal.__version__)
#app.route("/login")
def login():
session["state"] = str(uuid.uuid4())
# Technically we could use empty list [] as scopes to do just sign in,
# here we choose to also collect end user consent upfront
auth_url = _build_auth_url(scopes=app_config.SCOPE, state=session["state"])
return render_template("login.html", auth_url=auth_url, version=msal.__version__)
#app.route(app_config.REDIRECT_PATH) # Its absolute URL must match your app's redirect_uri set in AAD
def authorized():
if request.args.get('state') != session.get("state"):
return redirect(url_for("index")) # No-OP. Goes back to Index page
if "error" in request.args: # Authentication/Authorization failure
return render_template("auth_error.html", result=request.args)
if request.args.get('code'):
cache = _load_cache()
result = _build_msal_app(cache=cache).acquire_token_by_authorization_code(
request.args['code'],
scopes=app_config.SCOPE, # Misspelled scope would cause an HTTP 400 error here
redirect_uri=url_for("authorized", _external=True))
if "error" in result:
return render_template("auth_error.html", result=result)
session["user"] = result.get("id_token_claims")
_save_cache(cache)
return redirect(url_for("index"))
#app.route("/logout")
def logout():
session.clear() # Wipe out user and its token cache from session
return redirect( # Also logout from your tenant's web session
app_config.AUTHORITY + "/oauth2/v2.0/logout" +
"?post_logout_redirect_uri=" + url_for("index", _external=True))
#app.route("/graphcall")
def graphcall():
token = _get_token_from_cache(app_config.SCOPE)
if not token:
return redirect(url_for("login"))
graph_data = requests.get( # Use token to call downstream service
app_config.ENDPOINT,
headers={'Authorization': 'Bearer ' + token['access_token']},
).json()
return render_template('display.html', result=graph_data)
def _load_cache():
cache = msal.SerializableTokenCache()
if session.get("token_cache"):
cache.deserialize(session["token_cache"])
return cache
def _save_cache(cache):
if cache.has_state_changed:
session["token_cache"] = cache.serialize()
def _build_msal_app(cache=None, authority=None):
return msal.ConfidentialClientApplication(
app_config.CLIENT_ID, authority=authority or app_config.AUTHORITY,
client_credential=app_config.CLIENT_SECRET, token_cache=cache)
def _build_auth_url(authority=None, scopes=None, state=None):
return _build_msal_app(authority=authority).get_authorization_request_url(
scopes or [],
state=state or str(uuid.uuid4()),
redirect_uri=url_for("authorized", _external=True))
def _get_token_from_cache(scope=None):
cache = _load_cache() # This web app maintains one cache per session
cca = _build_msal_app(cache=cache)
accounts = cca.get_accounts()
if accounts: # So all account(s) belong to the current signed-in user
result = cca.acquire_token_silent(scope, account=accounts[0])
_save_cache(cache)
return result
app.jinja_env.globals.update(_build_auth_url=_build_auth_url) # Used in template
if __name__ == "__main__":
app.run()
I am one of the maintainer of the Azure AD authentication example from here: https://github.com/Azure-Samples/ms-identity-python-webapp
and I am the author of my workaround repo https://github.com/rayluo/flask-session
As of this writing, an indirect upstream module, cachelib, has released a bugfix for this 2 days ago, at about the same time you encountered this issue. So, to any future readers here: you should not have to explicitly switch to use the aforementioned workaround. That Azure AD web app sample will properly declare the right dependencies it needs.
I found the solution, or more accuratly my failing:
This repo:
https://github.com/rayluo/flask-session
Is not the same as this one:
https://github.com/fengsp/flask-session
The included requirements.txt loads the newer/maintained flask-session from rayluo (thanks!) rather than the one from pip.
So for anyone else finding this, make sure you use the right version!
I hope that somebody can help me.
I have to write a unit test with unittest of Python in a flask api. I have a login route that works perfectly fine when accessing it through the app with a React frontend but whenever I tried to post from the test, the request.authorization is None... It drives me crazy
I looked all over the internet and tried a lot of different approach but whatever I do, request.authorization is always None when doing a test
Testing :
import unittest
import base64
from backend.peace_api import app
class TestLogin(unittest.TestCase):
# Assert login() with correct authentication
def test_login(self):
with app.app_context():
tester = app.test_client(self)
auth = 'seo#hotmail.com:password'
authheader = base64.b64encode(bytes(auth, 'UTF-8'))
headers = {"HTTP_AUTHORIZATION": "Bearer " + str(authheader), "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8"}
response = tester.post('/api/login/', headers=dict(headers))
print(response.json)
self.assertEqual(response.status_code, 200)
if __name__ == '__main__':
unittest.main()
Route :
import jwt
import datetime
from flask import Blueprint, request, jsonify
from backend.peace_api import database, secret_key
from backend.peace_api.flat.models.flat import Flat
login_blueprint = Blueprint("login", __name__)
#login_blueprint.route("/", methods=["POST"])
def login():
auth = request.authorization # This here is always None
print("Hello World")
print(request)
print(request.authorization)
if auth is None:
return jsonify({"success": False}, 401)
email = auth.username
password = auth.password
if email is None or email is None or password is None:
return jsonify({"success": False}, 500)
mongo_flat = database.flats.find_one({"email": email})
if mongo_flat is not None:
flat = Flat(
mongo_flat["_id"],
mongo_flat["name"],
mongo_flat["email"],
mongo_flat["password"],
mongo_flat["tasks"],
mongo_flat["members"],
)
if password == flat.password and email == flat.email:
token = jwt.encode(
{
"id": str(flat.id),
"exp": datetime.datetime.utcnow() + datetime.timedelta(minutes=30),
},
secret_key,
)
return jsonify({"token": token.decode("UTF-8")})
else:
return jsonify({"success": False}, 401)
else:
return jsonify({"success": False}, 401)
Printed message :
Testing started at 19:15 ...
Launching unittests with arguments python -m unittest test_login.TestLogin in [...]\tests
Hello World
<Request 'http://localhost/api/login/' [POST]>
None
Ran 1 test in 0.017s
OK
[{'success': False}, 401]
I have honestly no clue what I should do... Thanks for the help
So there are a few issues with your setup which are resulting in the header not being sent or being sent but being malformed.
The name of the header is "Authorization", not "HTTP_AUTHORIZATION".
The credentials value for the Authorization header needs to be base64 encoded per the spec.
The default authorization middleware for Werkzeug only supports Basic auth, so your Bearer token will not work unless you're using an extension that adds Bearer support to Werkzeug (without knowing more about your setup it's hard to know what's going on there).
Here's a very simplified Flask App that demonstrates a working test client with a functioning Authorization header:
import flask
import base64
app = flask.Flask("app")
#app.route("/")
def test():
print(flask.request.authorization)
return "Worked"
with app.test_client() as c:
c.get("/", headers={"Authorization": "Basic {}".format(base64.b64encode(b"useo#hotmail.com:pass").decode("utf8"))})
Which prints:
{'password': 'pass', 'username': 'seo#hotmail.com'}
<Response streamed [200 OK]>
A similar question was asked here:
Flask werkzeug request.authorization is none but Authorization headers present
I have a python Twilio code like this(Click to Call method in twilio):
from flask import Flask
from flask import jsonify
#from flask import render_template
#from flask import request
from flask import url_for
from twilio.twiml.voice_response import VoiceResponse
from twilio.rest import Client
app = Flask(__name__)
# Voice Request URL
#app.route('/call')
def call():
# Get phone number we need to call
phone_number = request.form.get('phoneNumber', None)
try:
twilio_client = Client(app.config['TWILIO_ACCOUNT_SID'],
app.config['TWILIO_AUTH_TOKEN'])
except Exception as e:
msg = 'Missing configuration variable: {0}'.format(e)
return jsonify({'error': msg})
try:
twilio_client.calls.create(from_=app.config['TWILIO_CALLER_ID'],
to=phone_number,
url=url_for('.outbound', _external=True))
except Exception as e:
app.logger.error(e)
return jsonify({'error': str(e)})
return jsonify({'message': 'Call incoming!'})
#app.route('/outbound', methods=['POST'])
def outbound():
response = VoiceResponse()
response.say("Thank you for contacting our sales department. If this "
"click to call application was in production, we would "
"dial out to your sales team with the Dial verb.",
voice='alice')
response.number("+16518675309")
return str(response)
if __name__ == '__main__':
app.run()
When i try run run this from browser by calling : http://localhost:5000/call
i am getting ERROR: Unable to create record: Url is not a valid url:
How to call the Outbound function in the url and start the conversation between two people.
Instead of url_for('.outbound', _external=True) you should use url_for('outbound'). The docs linked by stamaimer say:
In case blueprints are active you can shortcut references to the same blueprint by prefixing the local endpoint with a dot (.).
You do not need a dot at the beginning. Check how url building is handled in flask.
I'm currently testing Google Search Console API and Flask Dance to do the oauth stuff.
It works great for getting the /sites, but i get an
ValueError: View function did not return a response
if i try to query searchAnalytics with /search
According to https://developers.google.com/webmaster-tools/search-console-api-original/v3/searchanalytics/query#try-it it must be POST + additonal data. e.g.:
json={'startDate':'2017-11-01','endDate':'2017-12-01'}
In https://developers.google.com/oauthplayground/ it works like this but with flask dance sadly not. any ideas?
import os
from werkzeug.contrib.fixers import ProxyFix
from flask import Flask, redirect, url_for
from flask_dance.contrib.google import make_google_blueprint, google
from raven.contrib.flask import Sentry
app = Flask(__name__)
app.wsgi_app = ProxyFix(app.wsgi_app)
sentry = Sentry(app)
app.secret_key = os.environ.get("FLASK_SECRET_KEY", "supersekrit")
app.config["GOOGLE_OAUTH_CLIENT_ID"] = os.environ.get("GOOGLE_OAUTH_CLIENT_ID")
app.config["GOOGLE_OAUTH_CLIENT_SECRET"] = os.environ.get("GOOGLE_OAUTH_CLIENT_SECRET")
google_bp = make_google_blueprint(scope=["profile", "email", "https://www.googleapis.com/auth/webmasters"])
app.register_blueprint(google_bp, url_prefix="/login")
#app.route("/")
def index():
return "BlA BLA"
#app.route("/sites")
def sites():
if not google.authorized:
return redirect(url_for("google.login"))
resp = google.get("/webmasters/v3/sites")
siteEntry = resp.json()["siteEntry"]
result = ""
for site in siteEntry:
result = result + site["siteUrl"] + "</br>"
return result
#app.route("/search")
def search():
if not google.authorized:
return redirect(url_for("google.login"))
resp = google.post("/webmasters/v3/sites/https%3A%2F%2Fzrce.eu/searchAnalytics/query", json={'startDate':'2017-11-01','endDate':'2017-12-01'})
print(resp)
if __name__ == "__main__":
app.run()
All API routes should always return something even if its an empty response.
Simply return a response, it can be "" or resp.
I tried to use flask_oauthlib to access my twitter api, but all I get is the error : Failed to generate request token. Here is the code.
from flask_oauthlib.client import OAuth
from flask import Flask, url_for, request, jsonify
app = Flask(__name__)
oauth = OAuth()
twitter = oauth.remote_app(
'twitter',
base_url='https://api.twitter.com/1/',
request_token_url='https://api.twitter.com/oauth/request_token',
access_token_url='https://api.twitter.com/oauth/access_token',
authorize_url='https://api.twitter.com/oauth/authorize',
consumer_key='dOJjyxB6gxXWTjdtfPUZcZPjl',
consumer_secret='im not telling you',
)
#app.route('/login')
def login():
return twitter.authorize(callback=url_for('authorized',
next=request.args.get('next') or request.referrer or None))
#app.route('/authorized')
#twitter.authorized_handler
def authorized(resp):
if resp is None:
return 'Access denied: error=%s' % (
request.args['error']
)
if 'oauth_token' in resp:
# session['example_oauth'] = resp
print(resp)
return jsonify(resp)
return str(resp)
if __name__ == '__main__':
app.run(port=8000, debug=True)
This didn't work while using http://term.ie/oauth/example/client.php, I managed to get a request token.
I inspired myself with https://github.com/lepture/example-oauth1-server/blob/master/client.py and http://flask-oauthlib.readthedocs.io/en/latest/client.html
EDIT
Weird fact : I tried the code here : https://github.com/lepture/flask-oauthlib/blob/master/example/twitter.py
I didn't changed the key and secret and it worked.
So I tried to change them for my own credentials, and it stopped working. I really can't understand...
Ok I found the problem. It appears that the callback URL is mandatory when using flask-oauthlib. So I added a fake one since i'm still on localhost, and it solved this problem.
In case anyone found this issue. I'm the author of Flask-OAuthlib. I suggest that you use Authlib instead, browser the source code at https://github.com/lepture/authlib. There are many built-in social connections in https://github.com/authlib/loginpass.