So I've been using Flask to create an app that uses Spotify API, and the authorization code flow works perfectly fine when running on my localhost server. However, after deploying the app to Heroku, occasionally the app will crash and give me a 'token not provided' error in the logs. Sometimes it'll work flawlessly, but when I play around with it more it'll crash. I try to redo the process on my localhost but it doesn't seem to break. I'm storing the token in sessions, could it be that Heroku is having problem with retrieving session variables?
Here's the flask app code
#Home view
#app.route('/')
#app.route('/home')
def home():
return render_template('home.html', title='Home')
#Login view
#app.route('/login', methods=['GET','POST'])
def login():
AUTH_FIRST = req_auth()
return redirect(AUTH_FIRST)
#Callback view for Spotify API
#app.route('/callback')
def callback():
if request.args.get('code'):
code = request.args.get('code')
token = req_token(code)
session['token'] = token
return redirect(url_for('generate_playlist'))
else:
return redirect(url_for('home'))
#Generate playlist view
#app.route('/generate_playlist', methods=['GET', 'POST'])
def generate_playlist():
if request.method == 'POST':
levels = int(float(request.form['level']))
token = session.get('token')
generate(token, levels)
return redirect(url_for('success'))
else:
return redirect(url_for('generate_playlist'))
This is the backend authorization code (I've commented out some parts to make it simpler, yet it still doesn't work):
client_id = os.environ.get("CLIENT_ID")
client_secret = os.environ.get("CLIENT_SECRET")
redirect_uri = os.environ.get('REDIRECT_URI')
state = ''.join(random.choice(string.ascii_lowercase + string.digits) for n in range(8))
scope = 'user-top-read user-library-read playlist-modify-public'
def req_auth():
show_dialog = "false"
AUTH_FIRST_URL = f'https://accounts.spotify.com/authorize?client_id={client_id}&response_type=code&redirect_uri={quote("https://surprisify-playlist-generator.herokuapp.com/callback")}&show_dialog={show_dialog}&state={state}&scope={scope}'
return AUTH_FIRST_URL
def req_token(code):
#B64 encode variables
client_creds = f"{client_id}:{client_secret}"
client_creds_b64 = base64.b64encode(client_creds.encode())
#Token data
token_data = {
"grant_type": "authorization_code",
"code": code,
"redirect_uri": "https://surprisify-playlist-generator.herokuapp.com/callback"
}
#Token header
token_header = {
"Authorization": f"Basic {client_creds_b64.decode()}"
}
#Make request post for token info
token_json = requests.post('https://accounts.spotify.com/api/token', data=token_data, headers=token_header)
return token_json.json()['access_token']
#Checking if token is still valid, otherwise, refresh
# if token_json.json()['expires_in']:
# now = datetime.datetime.now()
# expires_in = token_json.json()['expires_in']
# expires = now + datetime.timedelta(seconds=expires_in)
# if now > expires:
# refresh_token_data = {
# "grant_type": "refresh_token",
# "refresh_token": token_json.json()['refresh_token']
# }
# refresh_token_json = requests.post('https://accounts.spotify.com/api/token', data=refresh_token_data, headers=token_header)
# token = refresh_token_json.json()['access_token']
# return token
# else:
# token = token_json.json()['access_token']
# return token
# else:
# token = token_json.json()['access_token']
# return token
Could the error happen after you restart the browser? Flask session cookies are stored (by default) until you close the browser. Then, if you don't authenticate again, calling session.get(token) will return None and is likely why you get the error. You could try checking if token is None in generate_playlist() and requiring re-authentication with a redirect.
Here is an implementation of the Spotify authorization code flow using Spotipy that I have used with success in my own project: https://stackoverflow.com/a/57929497/6538328 (method 2).
Related
I want to get result from app route function in Flask and set to independent variable to call this variable in another places:
token = ''
#app.route('/api/data/', methods=['POST'])
#cross_origin()
#authentication_required(False)
def get_data():
token = get_token()
req_str = request.data.decode()
req_str = req_str.replace("'", '"')
json_data = json.loads(req_str)
res = get_city_list(json_data)
b = Response(json.dumps(res), status=201, mimetype='application/json')
return b, token
with app.app_context():
a = get_data()
print(a)
token = a
print(token)
but I have this mistake :
RuntimeError: Working outside of request context.
This typically means that you attempted to use functionality that needed
an active HTTP request. Consult the documentation on testing for
information about how to avoid this problem.
Errors:
Access to fetch at 'https://login.microsoftonline.com/{bunchofinfo}' (redirected from 'http://localhost:5000/login') from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
GET https://login.microsoftonline.com/{bunchofinfo} net::ERR_FAILED 200
Code:
#app.route("/")
def index():
if not session.get("user"):
return redirect(url_for("login"))
return redirect('http://localhost:3000/')
#app.route("/login")
def login():
session["flow"] = _build_auth_code_flow(scopes=SCOPE_MSAL)
auth_url = session["flow"]["auth_uri"]
print(auth_url)
return redirect(auth_url)
Things Ive tried:
First tried to set {'mode': 'no-cors'} and redirect. But it gives me the HTML for the redirect and doesn't redirect me.
Then tried sending my frontend the auth_url with no redirect. Then window.location.assign(auth_url)
Problem = Once logged on properly, it stays on localhost:5000/login with the auth_url on the page.
Tried to switch code with this but it doesn't move on after login and runs the if not statement.
def login():
if not session.get("user"):
session["flow"] = _build_auth_code_flow(scopes=SCOPE_MSAL)
auth_url = session["flow"]["auth_uri"]
print(auth_url)
return auth_url
return redirect('http://localhost:3000')
Don't know how to proceed. Rest of code from MSAL
#app.route("/getAToken")
def authorized():
try:
cache = _load_cache()
result = _build_msal_app(cache=cache).acquire_token_by_auth_code_flow(
session.get("flow", {}), request.args)
if "error" in result:
print(result)
# add error page
# return render_template("auth_error.html", result=result)
session["user"] = result.get("id_token_claims")
_save_cache(cache)
except ValueError: # Usually caused by CSRF
pass # Simply ignore them
return redirect(url_for("index"))
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(
microsoft_client_id, authority=authority or AUTHORITY,
client_credential=client_secret, token_cache=cache)
def _build_auth_code_flow(authority=None, scopes=None):
return _build_msal_app(authority=authority).initiate_auth_code_flow(
scopes or [],
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
This is driving me absolutely crazy and preventing me from being able to do local dev/test.
I have a flask app that uses authlib (client capabilities only). When a user hits my home page, my flask backend redirects them to /login which in turn redirects to Google Auth. Google Auth then posts them back to my app's /auth endpoint.
For months, I have been experiencing ad-hoc issues with authlib.integrations.base_client.errors.MismatchingStateError: mismatching_state: CSRF Warning! State not equal in request and response. It feels like a cookie problem and most of the time, I just open a new browser window or incognito or try to clear cache and eventually, it sort of works.
However, I am now running the exact same application inside of a docker container and at one stage this was working. I have no idea what I have changed but whenever I browse to localhost/ or 127.0.0.1/ and go through the auth process (clearing cookies each time to ensure i'm not auto-logged in), I am constantly redirected back to localhost/auth?state=blah blah blah and I experience this issue:
authlib.integrations.base_client.errors.MismatchingStateError: mismatching_state: CSRF Warning! State not equal in request and response.
I think the relevant part of my code is:
#app.route("/", defaults={"path": ""})
#app.route("/<path:path>")
def catch_all(path: str) -> Union[flask.Response, werkzeug.Response]:
if flask.session.get("user"):
return app.send_static_file("index.html")
return flask.redirect("/login")
#app.route("/auth")
def auth() -> Union[Tuple[str, int], werkzeug.Response]:
token = oauth.google.authorize_access_token()
user = oauth.google.parse_id_token(token)
flask.session["user"] = user
return flask.redirect("/")
#app.route("/login")
def login() -> werkzeug.Response:
return oauth.google.authorize_redirect(flask.url_for("auth", _external=True))
I would hugely appreciate any help.
When I run locally, I start with:
export FLASK_APP=foo && flask run
When I run inside docker container, i start with:
.venv/bin/gunicorn -b :8080 --workers 16 foo
Issue was that SECRET_KEY was being populated using os.random which yielded different values for different workers and thus, couldn't access the session cookie.
#adamcunnington here is how you can debug it:
#app.route("/auth")
def auth() -> Union[Tuple[str, int], werkzeug.Response]:
# Check these two values
print(flask.request.args.get('state'), flask.session.get('_google_authlib_state_'))
token = oauth.google.authorize_access_token()
user = oauth.google.parse_id_token(token)
flask.session["user"] = user
return flask.redirect("/")
Check the values in request.args and session to see what's going on.
Maybe it is because Flask session not persistent across requests in Flask app with Gunicorn on Heroku
How I Fix My Issue
install old version of authlib it work fine with fastapi and flask
Authlib==0.14.3
For Fastapi
uvicorn==0.11.8
starlette==0.13.6
Authlib==0.14.3
fastapi==0.61.1
Imporantt if using local host for Google auth make sure get https certifcate
install chocolatey and setup https check this tutorial
https://dev.to/rajshirolkar/fastapi-over-https-for-development-on-windows-2p7d
ssl_keyfile="./localhost+2-key.pem" ,
ssl_certfile= "./localhost+2.pem"
--- My Code ---
from typing import Optional
from fastapi import FastAPI, Depends, HTTPException
from fastapi.openapi.docs import get_swagger_ui_html
from fastapi.openapi.utils import get_openapi
from starlette.config import Config
from starlette.requests import Request
from starlette.middleware.sessions import SessionMiddleware
from starlette.responses import HTMLResponse, JSONResponse, RedirectResponse
from authlib.integrations.starlette_client import OAuth
# Initialize FastAPI
app = FastAPI(docs_url=None, redoc_url=None)
app.add_middleware(SessionMiddleware, secret_key='!secret')
#app.get('/')
async def home(request: Request):
# Try to get the user
user = request.session.get('user')
if user is not None:
email = user['email']
html = (
f'<pre>Email: {email}</pre><br>'
'documentation<br>'
'logout'
)
return HTMLResponse(html)
# Show the login link
return HTMLResponse('login')
# --- Google OAuth ---
# Initialize our OAuth instance from the client ID and client secret specified in our .env file
config = Config('.env')
oauth = OAuth(config)
CONF_URL = 'https://accounts.google.com/.well-known/openid-configuration'
oauth.register(
name='google',
server_metadata_url=CONF_URL,
client_kwargs={
'scope': 'openid email profile'
}
)
#app.get('/login', tags=['authentication']) # Tag it as "authentication" for our docs
async def login(request: Request):
# Redirect Google OAuth back to our application
redirect_uri = request.url_for('auth')
print(redirect_uri)
return await oauth.google.authorize_redirect(request, redirect_uri)
#app.route('/auth/google')
async def auth(request: Request):
# Perform Google OAuth
token = await oauth.google.authorize_access_token(request)
user = await oauth.google.parse_id_token(request, token)
# Save the user
request.session['user'] = dict(user)
return RedirectResponse(url='/')
#app.get('/logout', tags=['authentication']) # Tag it as "authentication" for our docs
async def logout(request: Request):
# Remove the user
request.session.pop('user', None)
return RedirectResponse(url='/')
# --- Dependencies ---
# Try to get the logged in user
async def get_user(request: Request) -> Optional[dict]:
user = request.session.get('user')
if user is not None:
return user
else:
raise HTTPException(status_code=403, detail='Could not validate credentials.')
return None
# --- Documentation ---
#app.route('/openapi.json')
async def get_open_api_endpoint(request: Request, user: Optional[dict] = Depends(get_user)): # This dependency protects our endpoint!
response = JSONResponse(get_openapi(title='FastAPI', version=1, routes=app.routes))
return response
#app.get('/docs', tags=['documentation']) # Tag it as "documentation" for our docs
async def get_documentation(request: Request, user: Optional[dict] = Depends(get_user)): # This dependency protects our endpoint!
response = get_swagger_ui_html(openapi_url='/openapi.json', title='Documentation')
return response
if __name__ == '__main__':
import uvicorn
uvicorn.run(app, port=8000,
log_level='debug',
ssl_keyfile="./localhost+2-key.pem" ,
ssl_certfile= "./localhost+2.pem"
)
.env file
GOOGLE_CLIENT_ID=""
GOOGLE_CLIENT_SECRET=""
Google Console Setup
I have a project that use flask in backend and jinja and html for frontend.
What i need is to send a request that has an authorization header and all my routes read that header to see if its from an valid user or not?
def show_admin():
data = request.headers.get('Authorization')
# data = "TOKEN123"
# واکشی اطلاعات مورد نیاز صفحه داشبورد
content = {
'user': mydb.selectalluser(),
'doreh': mydb.selectalldoreh()
}
# چک میشود اگر توکن ارسالی توسط کاربری معتبر است یا خیر
if str(data) == "TOKEN123":
return render_template('admin/dashboard.html', content=content)
# return hello
else:
# اگر توکن معتبر باشد صفحه لود خواهد شد
return render_template('login/index.html')
In the if statement it check if the token is valid or not. but...
1. how to generate a request that included Authorization header
2. how to generate a token for login page
You can't control the client-side request header (i.e. Authorization) on the server-side. IMO, what you want is manage user login/session status, you can implement this with Flask session, the information stored on session can be obtained between requests (use Cookie):
from flask import session
#app.route('/login')
def login():
session['token'] = 'TOKEN123' # store token, use it as a dict
return 'login success'
#app.route('/admin')
def show_admin():
if session.get('token') == "TOKEN123": # get token
return 'admin page'
else:
return 'login page'
However, I would recommend using Flask-Login to handle user session manganment.
Having some trouble using this plugin https://github.com/agile4you/bottle-jwt/
It seems to not work as I expected, down below my code:
import bottle
from Py.engine import *
from bottle_jwt import (JWTProviderPlugin, jwt_auth_required)
class AuthBackend(object):
user = {'id': 1237832, 'username': 'pav', 'password': '123', 'data': {'sex': 'male', 'active': True}}
def authenticate_user(self, username, password):
"""Authenticate User by username and password.
Returns:
A dict representing User Record or None.
"""
if username == self.user['username'] and password == self.user['password']:
return self.user
return None
def get_user(self, user_id):
"""Retrieve User By ID.
Returns:
A dict representing User Record or None.
"""
if user_id == self.user['id']:
return {k: self.user[k] for k in self.user if k != 'password'}
return None
app = bottle.Bottle()
server_secret = 'secret'
provider_plugin = JWTProviderPlugin(
keyword='jwt',
auth_endpoint='/login',
backend=AuthBackend(),
fields=('username', 'password'),
secret=server_secret,
ttl=30
)
app.install(provider_plugin)
#app.route('/')
#jwt_auth_required
def index():
return open('Html/index.html', 'r').read()
#app.post('/login')
def login():
return open('Html/login.html', 'r').read()
#app.get('/login')
def login():
return open('Html/login.html', 'r').read()
def run_server():
bottle.run(app=app, host='localhost', port=8080, debug=True, reloader=True)
# Main
if __name__ == '__main__':
run_server()
Once running, if I open browser On 127.0.0.1/8080 i get back a blank page with the string "{"AuthError": ["Cannot access this resource!"]}"
Which is Fine, it means that I'm not allowed to open index.html file (Cool: #jwt_auth_required worked)
Digging in source file I found a function named validate_token() with:
if not token:
logger.debug("Forbidden access")
raise JWTForbiddenError('Cannot access this resource!')
Here is the exception
except JWTForbiddenError as error:
bottle.response.content_type = b('application/json')
bottle.response._status_line = b('403 Forbidden')
return {"AuthError": error.args}
So, is there any way to redirect me on my login.html page if token does not match or is absent?
Plugin includes some way to do that or is just an API pckg?
That's not how JWT concept is supposed to be used. JWT are for RESTFul.
You need to make the server as REST API and on the client use JS
libraries such as AngularJs / Vue.js etc.,
Coming to the question about the plugin:
provider_plugin = JWTProviderPlugin(
keyword='jwt',
auth_endpoint='/login',
backend=AuthBackend(),
fields=('username', 'password'),
secret=server_secret,
ttl=30
)
auth_endpoint='/login' is to give a custom endpoint for authorization where the Bottle_JWT methods are looking for credentials to validate and generate JWT for.
I created a mock just to construct a response and this is how it should be used.
Once you pass the correct credential, the plugin responds with the JWT and expire which you have to intercept in authorized calls and add as request headers
Hope this helps.