justpy icon indicating copy to clipboard operation
justpy copied to clipboard

Problem with OAuth2

Open glorietaru opened this issue 4 years ago • 10 comments

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 with http rather than https.)
  • Is this a known issue or am I using justpy incorrectly?

glorietaru avatar Dec 01 '20 11:12 glorietaru

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.

elimintz avatar Dec 01 '20 16:12 elimintz

Sorry only just seen this. Thanks for the reply. Will attempt and get back to you.

glorietaru avatar Dec 10 '20 19:12 glorietaru

Yes, it works.

99hats avatar Jan 05 '21 03:01 99hats

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.

glorietaru avatar Jan 11 '21 10:01 glorietaru

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.

elimintz avatar Jan 11 '21 15:01 elimintz

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

glorietaru avatar Jan 11 '21 16:01 glorietaru

Try changing wp.redirect(authorization_url) to wp.redirect = authorization_url

elimintz avatar Jan 11 '21 16:01 elimintz

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.

glorietaru avatar Jan 11 '21 16:01 glorietaru

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.

elimintz avatar Jan 11 '21 16:01 elimintz

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()

glorietaru avatar Jan 12 '21 11:01 glorietaru