justpy
justpy copied to clipboard
Problem with OAuth2
OAuth2 fails to authenticate using justpy redirect.
- I have two versions of a simple Google OAuth2 authenticator. The Flask one works but the JustPy one fails.
- This line (with a valid client ID) is not redirecting correctly:
jp.redirect("https://accounts.google.com/o/oauth2/auth?response_type=code&client_id=SOME_CLIENT_ID.apps.googleusercontent.com&redirect_uri=http%3A%2F%2Flocalhost%3A8000%2Foauth-authorized%2Fgoogle&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile&state=SOME_STATE&access_type=offline&include_granted_scopes=true")
- Google responds with:
Error 400: invalid_request
Missing required parameter: client_id
- Exactly the same code using a Flask server and
flask.redirect
is authenticated correctly by Google. - (I have
os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1'
in both scripts to allow testing withhttp
rather thanhttps
.) - Is this a known issue or am I using justpy incorrectly?
Are you redirecting from an event handler? If so, assuming that the second argument of the event handler is msg
, try the following:
msg.page.redirect = "https://accounts.google.com/o/oauth2/auth?response_type=code&client_id=SOME_CLIENT_ID.apps.googleusercontent.com&redirect_uri=http%3A%2F%2Flocalhost%3A8000%2Foauth-authorized%2Fgoogle&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile&state=SOME_STATE&access_type=offline&include_granted_scopes=true"
Please let me know how it goes.
Sorry only just seen this. Thanks for the reply. Will attempt and get back to you.
Yes, it works.
I'm redirecting from the root url, as:
client_id = os.environ.get("GOOGLE_CLIENT_ID")
scope = [
"https://www.googleapis.com/auth/userinfo.email",
"https://www.googleapis.com/auth/userinfo.profile"
]
redirect_uri = "http://localhost:8000/oauth-authorized/google"
jp.Route("/", login)
def login():
google = OAuth2Session(client_id=client_id, scope=scope, redirect_uri=redirect_uri)
authorization_url, state = google.authorization_url(authorization_base_url)
return jp.redirect(authorization_url)
Is there some way I can interpose an event handler in this process?
NB:
jp.WebPage().redirect(authorization_url)
fails with TypeError: 'NoneType' object is not callable
.
This should work:
jp.Route("/", login)
def login():
wp = jp.WebPage()
jp.Div(a=wp) # Just so page is not empty
google = OAuth2Session(client_id=client_id, scope=scope, redirect_uri=redirect_uri)
authorization_url, state = google.authorization_url(authorization_base_url)
wp.redirect = authorization_url
return wp
Please let me know if there is still a problem.
Many thanks for the very fast reply. I'm seeing something baffling:
def login():
wp = jp.WebPage()
assert wp is not None
jp.Div(a=wp) # Just so page is not empty
google = OAuth2Session(client_id=client_id, scope=scope, redirect_uri=redirect_uri)
authorization_url, state = google.authorization_url(authorization_base_url)
assert wp is not None
wp.redirect(authorization_url)
return wp
- With reponse:
ERROR h11_impl: Exception in ASGI application
Traceback (most recent call last):
File "/home/debian/justpy/jp/lib/python3.7/site-packages/uvicorn/protocols/http/h11_impl.py", line 394, in run_asgi
result = await app(self.scope, self.receive, self.send)
File "/home/debian/justpy/jp/lib/python3.7/site-packages/uvicorn/middleware/proxy_headers.py", line 45, in __call__
return await self.app(scope, receive, send)
File "/home/debian/justpy/jp/lib/python3.7/site-packages/starlette/applications.py", line 112, in __call__
await self.middleware_stack(scope, receive, send)
File "/home/debian/justpy/jp/lib/python3.7/site-packages/starlette/middleware/errors.py", line 181, in __call__
raise exc from None
File "/home/debian/justpy/jp/lib/python3.7/site-packages/starlette/middleware/errors.py", line 159, in __call__
await self.app(scope, receive, _send)
File "/home/debian/justpy/jp/lib/python3.7/site-packages/starlette/middleware/gzip.py", line 18, in __call__
await responder(scope, receive, send)
File "/home/debian/justpy/jp/lib/python3.7/site-packages/starlette/middleware/gzip.py", line 35, in __call__
await self.app(scope, receive, self.send_with_gzip)
File "/home/debian/justpy/jp/lib/python3.7/site-packages/starlette/exceptions.py", line 82, in __call__
raise exc from None
File "/home/debian/justpy/jp/lib/python3.7/site-packages/starlette/exceptions.py", line 71, in __call__
await self.app(scope, receive, sender)
File "/home/debian/justpy/jp/lib/python3.7/site-packages/starlette/routing.py", line 582, in __call__
await route.handle(scope, receive, send)
File "/home/debian/justpy/jp/lib/python3.7/site-packages/starlette/routing.py", line 243, in handle
await self.app(scope, receive, send)
File "/home/debian/justpy/jp/lib/python3.7/site-packages/starlette/endpoints.py", line 30, in dispatch
response = await handler(request)
File "/home/debian/justpy/jp/lib/python3.7/site-packages/justpy/justpy.py", line 158, in get
load_page = func_to_run()
File "server.py", line 65, in login
wp.redirect(authorization_url)
TypeError: 'NoneType' object is not callable
Try changing wp.redirect(authorization_url)
to wp.redirect = authorization_url
Thanks again for the reply.
Now I get no python errors but the browser says:
Google
Authorization Error
Error 400: invalid_request
Missing required parameter: client_id
As I say, I am using the same OAuth2Session
configuration as is working in Flask.
Another option I can think of is to use the page_ready event handler https://justpy.io/tutorial/page_events/ The result should be like from the event handler and should work.
Thanks to your help I finally have this working. FYI:
# server.py
import os
from logbook import FileHandler, debug, error, warn, info
from datetime import datetime
from requests_oauthlib import OAuth2Session
from redislite import StrictRedis
import justpy as jp
import asyncio
os.environ["OAUTHLIB_RELAX_TOKEN_SCOPE"] = "1" # Prevent "Warning: Scope has changed".
log_handler = FileHandler(f"""{os.environ.get("HOME")}/justpy/logs/server.log""")
rds = StrictRedis(os.environ.get("REDISLITE_DB"), charset="utf-8", decode_responses=True)
print(f"""=====\nConnect to redis via: redis-cli -s {rds.socket_file}\n=====""")
client_id = os.environ.get("GOOGLE_CLIENT_ID")
client_secret = os.environ.get("GOOGLE_SECRET")
redirect_uri = f"""{os.environ.get("JUSTPY_URL")}/oauth-authorized/google"""
authorization_base_url = "https://accounts.google.com/o/oauth2/auth"
token_url = "https://accounts.google.com/o/oauth2/token"
scope = [
"https://www.googleapis.com/auth/userinfo.email",
"https://www.googleapis.com/auth/userinfo.profile"
]
def hello_world():
wp = jp.WebPage()
jp.Hello(a=wp)
return wp
def root():
wp = jp.WebPage()
jp.Div(a=wp) # Just so page is not empty
wp.on('page_ready', login)
return wp
async def login(self, msg):
rds.set("server_start", datetime.now().isoformat())
google = OAuth2Session(client_id=client_id, scope=scope, redirect_uri=redirect_uri)
authorization_url, state = google.authorization_url(authorization_base_url)
rds.set("oauth_state", state)
msg.page.redirect = authorization_url
def oauth_callback(request):
google = OAuth2Session(client_id, scope=scope, redirect_uri=redirect_uri, state=rds.get("oauth_state"))
# Fetch the access token
# google.fetch_token(token_url, client_secret=client_secret,
# authorization_response=request.url) # 'URL' object has no attribute 'lower'
google.fetch_token(token_url, client_secret=client_secret,
authorization_response=str(request.url))
r = google.get('https://www.googleapis.com/oauth2/v1/userinfo')
print(f"""r.content: {r.content}""")
return jp.redirect("/hello")
jp.Route("/", root)
jp.Route('/oauth-authorized/google', oauth_callback)
jp.Route('/hello', hello_world)
with log_handler.applicationbound():
jp.justpy()