Reputation: 382
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
Upvotes: 11
Views: 34195
Reputation: 4213
Another thing that can go wrong, specifically for the ClassLink OAuth provider:
If ClassLink is not configured correctly then the button in a user's launchpad home page for your app will link to your app without including "state" in the query string.
This can cause confusion if you have a "log in with ClassLink" button on your own site that works, but then some users report that they can't log in without mentioning that they're logging in using the button in ClassLink's site.
A possibility in this case, though I haven't tried it, may be to redirect the user to the beginning of your OAuth process when MismatchingStateError
is raised.
Upvotes: 0
Reputation: 33
I got the same error message with my implementation with django oauth client (Authlib). In my case, it was due to an invalid local domain as a redirect URL ('http://exampledomain:8000' instead of 'http://exampledomain.com:8000').
When I corrected it, it worked:
ultimapi = settings.oauth.create_client('ultimapi')
def user_authorization(request):
if (request.user.is_authenticated):
return (redirect('home'))
return ultimapi.authorize_redirect(request, 'http://exampledomain.com:8000')
def authorization_callback(request):
info = ultimapi.authorize_access_token(request)
print(info)
return (redirect('http://localhost'))
Upvotes: 0
Reputation: 616
well, this is the least recommended method, but nothing else worked for me.
In the oathlib\oath2\rfc6749\parameters.py, under "parse_authorization_code_response", I commented out the following lines:
if state and params.get('state', None) != state:
raise MismatchingStateError()
I'm not trying to integrate with a web app, simply writing a python script to build a youtube playlist.
Upvotes: 0
Reputation: 51
When running gunicorn with multiple workers this problem showed up (not when running it with only a single worker). Upon closer inspection I realized that some of the links created in flask were set like this: redirect_uri=url_for("callback", _external=True) Flask uses the currently served scheme, which in my case was "http" (running behind the gunicorn server). However, the gunicorn communication with the auth0 server (this was my issue where the csrf token error popped up) was done using https (served by gunicorn). Setting the following solved the problem: redirect_uri=url_for("callback", _external=True, _scheme="https").
I hope this helps someone.
Upvotes: 2
Reputation: 6738
I fixed this problem by hard-refreshing my app in my browser. It seems I had changed some of the client IDs in my code and restarted the app, but I was still clicking the login button from an out of date version of my own app in my browser.
Upvotes: 0
Reputation: 41
I have encountered the same problem in FastAPI. What works for me is setting the same secret key in both - sessionMiddleware and oauth.register - places:
In respective python module:
# Set up OAuth
config_data = {'GOOGLE_CLIENT_ID': GOOGLE_CLIENT_ID, 'GOOGLE_CLIENT_SECRET': GOOGLE_CLIENT_SECRET}
starlette_config = Config(environ=config_data)
oauth = OAuth(starlette_config)
oauth.register(
name='google',
server_metadata_url='https://accounts.google.com/.well-known/openid-configuration',
client_kwargs={'scope': 'openid email profile'},
authorize_state='Oe_Ef1Y38o1KSWM2R-s-Kg',### this string should be similar to the one we put while add sessions middleware
)
In main.py (or wherever you declare app = FastAPI()):
app.add_middleware(SessionMiddleware, secret_key="Oe_Ef1Y38o1KSWM2R-s-Kg")
Upvotes: 3
Reputation: 2101
install old version of authlib it work fine with fastapi and flask
Authlib==0.14.3
uvicorn==0.11.8
starlette==0.13.6
Authlib==0.14.3
fastapi==0.61.1
https://dev.to/rajshirolkar/fastapi-over-https-for-development-on-windows-2p7d
ssl_keyfile="./localhost+2-key.pem" ,
ssl_certfile= "./localhost+2.pem"
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>'
'<a href="/docs">documentation</a><br>'
'<a href="/logout">logout</a>'
)
return HTMLResponse(html)
# Show the login link
return HTMLResponse('<a href="/login">login</a>')
# --- 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"
)
GOOGLE_CLIENT_ID=""
GOOGLE_CLIENT_SECRET=""
Upvotes: 5
Reputation: 382
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.
Upvotes: 14
Reputation: 2422
@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
Upvotes: 3