Some of our Dash callbacks involve calling an external API which is authorized through OAuth (Authlib is being used). When a user signs out their OAuth token is removed from their current session. However, if that user has another tab open, the (ReactJS) AJAX callbacks can continue to be called from that tab but will ultimately fail as there is no longer a valid token.
Ideally what would happen is if the user is no longer authorized during a callback a 401 response will be returned and that would somehow trigger the browser to redirect to the Flask application root (which will now detect they need to login and redirect to the OAuth server for login)
In my callbacks I can include something similar to:
#dashapp.callback(
Output('some-div', 'children'),
Input('some-input', 'data')
)
def my_fancy_callback(inputdata):
if not session.get('token'):
raise SessionExpiredException
jsonResult = api.get('https://myapi.com/someinterestingdata')
return jsonResult
And then have a Flask error handler that catches this specific exception and returns a 401 response:
#app.errorhandler(SessionExpiredException)
def handle_SessionExpired_exception(e):
return Response("Access Denied", 401)
This indeed results in the AJAX call returning a 401 response (as seen on the browser network output). However, I'm not sure how I can hook into this call on the React/Browser side of things? With Dash I could look at adding some custom JavaScript files to maybe somehow intercept XMLHttpRequest globally but I was wondering if there was already built in support with Dash to accomplish this?
I am very new to Python/Flask/Dash/Plotly so if there is another best practice for client side error handling I would welcome any suggestions.
Protecting Dash apps on Flask behind OAuth is possible without the Dash Enterprise middleware but it's not intuitive to get it set up. I use an application factory method with the following components to protect dash applications.
This pattern will check auth status on all Dash layouts and callbacks without any specific callback code.
my_dash_app = Dash(
__name__,
server=flask_server,
url_base_pathname=f'/{base_pathname}/',
assets_folder=assets_folder,
meta_tags=[meta_viewport],
external_stylesheets=external_stylesheets,
suppress_callback_exceptions=True,
external_scripts=external_scripts
)
protect_dash_views(my_dash_app)
def protect_dash_views(my_dash_app):
for view_func in my_dash_app.server.view_functions:
if view_func.startswith(my_dash_app.config.url_base_pathname):
my_dash_app.server.view_functions[view_func] = azure_authentication_required(
my_dash_app.server.view_functions[view_func])
def azure_authentication_required(protected_function):
#wraps(protected_function)
def decorated_function(*args, **kwargs):
if not user_is_employee(): # flask session cookie has expired, force user to login again.
return redirect(url_for("login"))
if user_is_blocked_from_access():
return abort(403, 'You do not have permission to access This Application.') # prevents further code execution.
if azure_access_token_is_expired():
user_id = get_azure_user_id()
get_refresh_token_from_cache(user_id=user_id) # this routine aborts code 403 if not successful.
return protected_function(*args, **kwargs)
return decorated_function
Related
I am working on code to login to my app using Microsoft login using fastapi. I am able to call microsoft login page and after entering the login credentials i am getting the error "{"detail":"'state' parameter in callback request does not match our internal 'state', someone may be trying to do something bad."}"
**main.py**
from fastapi_sso.sso.microsoft import MicrosoftSSO
app1 = FastAPI()
microsoft_sso = MicrosoftSSO("client-id", "client-secret", "http://localhost:8000/dashboard")
#app1.get("/microsoft/login")
async def microsoft_login():
return await microsoft_sso.get_login_redirect()
#app1.get("/dashboard")
async def microsoft_callback(request: Request):
user_new = await microsoft_sso.verify_and_process(request) #getting error at this point
print(user_new.id)
print("Hello Microsoftttttttttttttttt")
I had the same problem with this SOO using the google one. The solution was to create a new pair of keys and use one when developing the platform locally and the other one being used only in production. After doing this, the issue faded away. Just like that.
You probably need to pass use_state=False in the SSO constructor:
microsoft_sso = MicrosoftSSO("client-id", "client-secret", "http://localhost:8000/dashboard", use_state=False)
If the application is distributed (i.e multiple web servers), or if it plans to handle multiple simultaneous SSO flows (i.e login request for user A, then login request for user B, then callback for A then callback for B), then the default use_state=True is a bad idea. No idea how much more secure is it, but the above reasons are enough for me to disable this "feature".
What it does is, when generating the redirect url it generates a state, stores it locally and passes it as a cookie to provider, then in callback it checks if the cookie matches local state.
I'm working on an app using the Spotify API but I'm a bit new to all of this. I'm trying to get the Authorization Code with Proof Key for Code Exchange (PKCE) (https://developer.spotify.com/documentation/general/guides/authorization-guide/#authorization-code-flow-with-proof-key-for-code-exchange-pkce)
My problem is how do I redirect the user to the query where he has to ACCEPT the authorization and make my app to wait until the user clicks on ACCEPT. When he does this, the user will be redirected and that new URL (as the docs said) will contain the authorization code that I need to then exchange it for an authorization token.
This is my function so far to get that authorization code:
def get_auth_code(self):
code_challenge = self.get_code_challenge_PKCE()
scopes_needed = "user-read-email%20user-read-private%20playlist-read-collaborative%20playlist-modify-public%20playlist-read-private%20playlist-modify-private%20user-library-modify%20user-library-read"
endpoint = "https://accounts.spotify.com/authorize"
query = f"{endpoint}?client_id={self.client_ID}&response_type=code&redirect_uri={self.redirect_uri}&scope={scopes_needed}&code_challenge_method=S256&code_challenge={code_challenge}"
webbrowser.open(query)
Set up a web server.
To programmatially extract the access tokens you need a web server to handle the redirection after the user logs in on Spotify (which you redirected them to). Now this server can be the user pasting the URI to an input field on a terminal, but obviously this isn't ideal for user experience. It leaves room for lots of mistakes.
I've authored a Spotify Web API client, whose internals might be useful for you to examine. For example, you can use Flask to construct the server. The main principle is using one endpoint (i.e. /login) to redirect (code 307 worked for me browsers won't remember it) the user to a callback (i.e. /callback) which recieves the code parameter with which you can request an access token.
OAuth2 can be a bit of a pain to implement locally, I know. In my library I also made a similar function that you are constructing using webbrowser, but it does have the manual copy-pasting quirk. To use functions you can define yourself for brevity, the gist of it is:
verifier = secrets.token_urlsafe(32) # for PKCE, not in my library yet
url = user_authorisation_url(scope, state, verifier)
# Communicate with the user
print('Opening browser for Spotify login...')
webbrowser.open(url)
redirected = input('Please paste redirect URL: ').strip()
code = parse_code_from_url(redirected)
state_back = parse_state_from_url(redirected)
assert state == state_back # For that added security juice
token = request_user_token(code, verifier)
I'm facing problem in emiting messages from RabbitMQ to User via SocketIO.
I have Flask application with SocketIO integration.
Current user flow seems like
The problem is i'm not able to set up RabbitMQ listener which forward messages to browser via SocketIO. Every time i'm getting different error. Mostly is that connection is closed, or i'm working outside of application context.
I tried many approaches, here is my last one.
# callback
def mq_listen(uid):
rabbit = RabbitMQ()
def cb(ch, method, properties, body, mq=rabbit):
to_return = [0] # mutable
message = Message.load(body)
to_return[0] = message.get_message()
emit('report_part', {"data": to_return[0]})
rabbit.listen('results', callback=cb, id=uid)
# this is the page, which user reach
#blueprint.route('/report_result/<uid>', methods=['GET'])
def report_result(uid):
thread = threading.Thread(target=mq_listen, args=(uid,))
thread.start()
return render_template("property/report_result.html", socket_id=uid)
where rabbit.listen method is abstraction like:
def listen(self, queue_name, callback=None, id=None):
if callback is not None:
callback_function = callback
else:
callback_function = self.__callback
if id is None:
self.channel.queue_declare(queue=queue_name, durable=True)
self.channel.basic_qos(prefetch_count=1)
self.consumer_tag = self.channel.basic_consume(callback_function, queue=queue_name)
self.channel.start_consuming()
else:
self.channel.exchange_declare(exchange=queue_name, type='direct')
result = self.channel.queue_declare(exclusive=True)
exchange_name = result.method.queue
self.channel.queue_bind(exchange=queue_name, queue=exchange_name, routing_key=id)
self.channel.basic_consume(callback_function, queue=exchange_name, no_ack=True)
self.channel.start_consuming()
which resulted into
RuntimeError: working outside of request context
I will be happy for any tip or example of usage.
Thanks a lot
I had a similar issue, in the end of the day it's because when you make a request flask passes the request context to client. But the solution is NOT to add with app.app_context(). That is hackey and will definitely have errors as you're not natively sending the request context.
My solution was to create a redirect so that the request context is maintained like:
def sendToRedisFeed(eventPerson, type):
eventPerson['type'] = type
requests.get('http://localhost:5012/zmq-redirect', json=eventPerson)
This is my redirect function, so whenever there is an event I'd like to push to my PubSub it goes through this function, which then pushes to that localhost endpoint.
from flask_sse import sse
app.register_blueprint(sse, url_prefix='/stream')
#app.route('/zmq-redirect', methods=['GET'])
def send_message():
try:
sse.publish(request.get_json(), type='greeting')
return Response('Sent!', mimetype="text/event-stream")
except Exception as e:
print (e)
pass
Now, whenever an event is pushed to my /zmq-redirect endpoint, it is redirected and published via SSE.
And now finally, just to wrap everything up, the client:
var source = new EventSource("/stream");
source.addEventListener(
"greeting",
function(event) {
console.log(event)
}
)
The error message suggests that it's a Flask issue. While handling requests, Flask sets a context, but because you're using threads this context is lost. By the time it's needed, it is no longer available, so Flask gives the "working outside of request context" error.
A common way to resolve this is to provide the context manually. There is a section about this in the documentation: http://flask.pocoo.org/docs/1.0/appcontext/#manually-push-a-context
Your code doesn't show the socketio part. But I wonder if using something like flask-socketio could simplify some stuff... (https://flask-socketio.readthedocs.io/en/latest/). I would open up the RabbitMQ connection in the background (preferably once) and use the emit function to send any updates to connected SocketIO clients.
I'm consistently getting an "Invalid response from Facebook" error when authenticating over Facebook with Oauthlib when building off of the sample code here.
I've outlined the sections of relevant code below.
Setup:
Setting up the Oauth request object.
Not pictured: Navigational routes and Flask app initialization.
from flask_oauthlib.client import OAuth, OAuthException
oauth = OAuth()
facebook = oauth.remote_app('facebook',
base_url='https://graph.facebook.com/',
request_token_url=None,
access_token_url='/oauth/access_token',
authorize_url='https://www.facebook.com/dialog/oauth',
consumer_key=config.get("app_id"),
consumer_secret=config.get("app_secret"),
request_token_params={'scope': 'public_profile,email'}
)
#facebook.tokengetter
def get_facebook_token():
if current_user.is_authenticated():
return current_user.get_facebook_token()
else:
return None
Login handler:
Sending users here in order to begin the process, with the url for the facebook callback appended to the root URL.
#app.route('/facebook/login')
def facebook_login():
return facebook.authorize(callback="http://example.com%s" % url_for('facebook_callback'))
Facebook callback, source of the issue:
From here I can garner that a code (presumably the token) is returned but Oauthlib fails to parse it correctly.
#app.route('/facebook/callback')
def facebook_callback(response):
response = facebook.authorized_response()
if response is None:
flash("You denied the request to sign in.", "error")
return redirect(url_for('index'))
if isinstance(response, OAuthException):
flash("Access denied: %s" % response.message, "error")
return redirect(url_for('index'))
# Request fails here, returns the redirect above.
From dumping the request args I can see fairly clearly that after being directed to Facebook and successfully connecting, there is a very long token being returned to the callback along the lines of '?code=1234567890-abcdefghijklmnop', however actually trying to authenticate with this fails with "Invalid response from Facebook".
Here is a sample request dump:
ImmutableMultiDict([('code', 'AQAPedwWavTb_cBx6NVy-qy3AL5YPr780oG5tA1LfITpVwkk-kr_4I0lG6B-vlhcYEubhuKhEM46bPs-1WpWUpJzcWfhkQ64lIkdLm9uSHSqeBHBM_6zw7SDpvVmGK-JKWBpAqRJuBKenl9zslQizthox96104iiul0uYQY67cmZgPXZi9uL-mcgZ5dRj387eKJIjNninBXxwCGgFkg5kLVHYt7t0ktUH58stYlxn2f98AXuSlrIvWsA5NeHsVbM8XY0XQrDrNbCvjDmEwHQGkZ3uZRbyaecN7MAi0bM0TrZzpuQ8j3M34DnQp_v9n4ktM4')])
Having used similar code based off of the Twitter sample that works, I'm thinking this could be a possible library bug due to Facebook API changes, but I would appreciate any pointers!
For anyone who stumbles upon this from Google in the future, I solved this in a solution that can be read here.
Hey there, I solved this issue in a very hacky way which I would not
recommend for production environments, but I eventually found the
issue a few days after my last message.
When you ask Facebook for an access token, it does NOT give you an
access token in the way you might expect. What I assumed to be a
failure on Facebook's side was instead a (perhaps intentional)
formatting error.
What you might expect:
http://example.com/callback?access_token=00000000000
or
http://example.com/callback with the access token passed as a POST
argument in the headers.
What actually happens is that Facebook responds like this:
http://example.com/callback?#access_token=0000000000
Because of this, it is -impossible- for any server side language
to parse it, as the access token will now only be visible to the
browser itself. It is not passed to the backend whatsoever.
Capturing the request:
#app.route('/facebook/translate', methods=['GET'])
def facebook_translate():
# Facebook responds with the access token as ?#access_token,
# rather than ?access_token, which is only accessible to the browser.
# This part is where things get really, really dumb.
return ''' <script type="text/javascript">
var token = window.location.href.split("access_token=")[1];
window.location = "/facebook/callback?access_token=" + token;
</script> '''
Proceeding as usual:
#app.route('/facebook/callback', methods=['GET', 'POST'])
def facebook_callback():
access_token = request.args.get("access_token")
if access_token == "undefined":
flash("You denied the request to sign in.", "error")
return redirect(url_for('index'))
graph = facebooksdk.GraphAPI(access_token)
profile = graph.get_object("me")
I'm trying to build a website using the Tornado framework that is using OpenID to authenticate with an OpenID authentication service, also built using Tornado.
I've created a child class based on tornado.auth.OpenIdMixin and am using that as the authenticaton handler in my Tornado app:
class MyMixin(tornado.auth.OpenIdMixin):
_OPENID_ENDPOINT = "http://myserver.com/openidserver"
def authorize_redirect(self, oauth_scope, callback_uri=None, ax_attrs=["name", "email", "language", "username"]):
callback_uri = callback_uri or self.request.uri
args = self._openid_args(callback_uri, ax_attrs=ax_attrs)
self.redirect(self._OPENID_ENDPOINT + "?" + urllib_parse.urlencode(args))
def get_authenticated_user(self, callback):
tornado.auth.OpenIdMixin.get_authenticated_user(self, callback)
The handler class for the application is
class AuthHandler(BaseHandler, MyMixin):
#tornado.web.asynchronous
def get(self):
if self.get_argument("openid.mode", None):
self.get_authenticated_user(self.async_callback(self._on_auth))
return
self.authenticate_redirect()
def _on_auth(self, user):
if not user:
self.send_error(500)
self.set_secure_cookie("user", tornado.escape.json_encode(user), expires_days=0.01)
self.redirect("/")
This code is based on what the GoogleMixin class is doing and an example I found online at http://technobeans.wordpress.com/2012/09/04/tornado-third-party-authentication/
For the OpenID server I started with server.py from https://github.com/openid/python-openid but after running into some issues I also tried https://github.com/mrjj/cito. In both servers I have an account available for me to login to but both times I'm having the same problem on the client side.
My goal is to have my client see that I'm not logged in, redirect me to the OpenID server, I authenticate there, give the client permission to authenticate me using the ID and be redirected to the client to move on from there.
But with both servers my client always fails in AuthHandler._on_auth() because the user parameter is None.
This is the output of Tornado running my client:
ERROR:root:Exception after headers written
Traceback (most recent call last):
File "/usr/lib/python2.7/dist-packages/tornado/web.py", line 897, in wrapper
return callback(*args, **kwargs)
File "website.py", line 67, in _on_auth
self.redirect("/")
File "/usr/lib/python2.7/dist-packages/tornado/web.py", line 412, in redirect
raise Exception("Cannot redirect after headers have been written")
Exception: Cannot redirect after headers have been written
After adding some debugging print statements I know that the HTTP/500 error I'm getting is because the user variable is empty.
I know I don't quite understand how OpenID is supposed to work, but I can't find a practical example of how OpenID is supposed to work. There's a few diagrams with lots of arrows back and forth, but nothing with a little more technical detail.
All freely available servers I've looked at appear to be providing the same basic set of functions.
Am I pointing to the correct URL for the authentication process to start?
Am I missing some step in my clients' OpenID handling and it is looking too much like what Google is doing with their OpenID+OAuth protocol?