Skip to content

Commit

Permalink
Improved Google Oauth with credentials (#811)
Browse files Browse the repository at this point in the history
* Fixed auth enforcer so uses plugins if available

* Better error catching in login module

* Ok

* Removed some bugs

* Improved the documentation
  • Loading branch information
lucadealfaro authored Oct 8, 2023
1 parent 557651b commit 397cfab
Show file tree
Hide file tree
Showing 5 changed files with 71 additions and 13 deletions.
13 changes: 8 additions & 5 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [

{
"name": "Python: py4web",
"type": "python",
Expand All @@ -15,7 +11,14 @@
"apps"
],
"console": "integratedTerminal",
"justMyCode": true,
"justMyCode": true
},
{
"name": "Python: File",
"type": "python",
"request": "launch",
"program": "${file}",
"justMyCode": true
}
]
}
2 changes: 1 addition & 1 deletion deployment_tools/gae/app.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
runtime: python37
runtime: python311

# Handlers define how to route requests to your application.
handlers:
Expand Down
6 changes: 3 additions & 3 deletions deployment_tools/gae/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
import site
site.addsitedir(os.path.join(os.path.dirname(__file__), 'lib'))
from py4web.core import Reloader, bottle, Session
os.environ['PY4WEB_DASHBOARD_MODE'] = 'demo'
os.environ['PY4WEB_DASHBOARD_MODE'] = 'none'
os.environ['PY4WEB_SERVICE_DB_URI'] = 'sqlite:memory'
os.environ['PY4WEB_APPS_FOLDER'] = os.path.join(os.path.dirname(__file__), 'apps')
os.environ['PY4WEB_SERVICE_FOLDER'] = os.path.join(os.path.dirname(__file__), 'apps/.service')
Session.SECRET = open(os.path.join(os.path.dirname(__file__), 'apps/.service/session.secret'), 'rb').read()
os.environ['PY4WEB_SERVICE_FOLDER'] = os.path.join(os.path.dirname(__file__), '.service')
Session.SECRET = open(os.path.join(os.path.dirname(__file__), '.service/session.secret'), 'rb').read()
Reloader.import_apps()
app = bottle.default_app()
5 changes: 3 additions & 2 deletions py4web/utils/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,7 @@ def __init__(
two_factor_required=two_factor_required,
two_factor_send=two_factor_send,
two_factor_tries=3,
auth_enforcer=None,
)

# callbacks for forms
Expand Down Expand Up @@ -469,11 +470,11 @@ def signature(self):
@property
def user(self):
"""Use as @action.uses(auth.user)"""
return AuthEnforcer(self)
return self.param.auth_enforcer if self.param.auth_enforcer else AuthEnforcer(self)

def condition(self, condition):
"""Use as @action.uses(auth.condition(lambda user: True))"""
return AuthEnforcer(self, condition)
return self.param.auth_enforcer if self.param.auth_enforcer else AuthEnforcer(self, condition)

# utilities
def get_user(self, safe=True):
Expand Down
58 changes: 56 additions & 2 deletions py4web/utils/auth_plugins/oauth2google_scoped.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,73 @@

import calendar
import json
import re
import time
import uuid

import google_auth_oauthlib.flow
import google.oauth2.credentials
from googleapiclient.discovery import build
from google.auth.exceptions import RefreshError

from py4web import request, redirect, URL, HTTP
from pydal import Field
from py4web.utils.auth import AuthEnforcer, REGEX_APPJSON


class AuthEnforcerGoogleScoped(AuthEnforcer):
"""This class catches certain invalid access errors Google generates
when credentials get stale, and forces the user to login again.
Pass it to Auth as param.auth_enfoercer, as in:
auth.param.auth_enforcer = AuthEnforcerGoogleScoped(auth)
"""

def __init__(self, auth, condition=None, error_page=None):
super().__init__(auth, condition=condition)
self.error_page = error_page
assert error_page is not None, "You need to specify an error page; can't use login."

def on_error(self, context):
if isinstance(context.get("exception"), RefreshError):
del context["exception"]
self.auth.session.clear()
if re.search(REGEX_APPJSON,
request.headers.get("accept", "")) and (
request.headers.get("json-redirects", "") != "on"
):
raise HTTP(403)
redirect_next = request.fullpath
if request.query_string:
redirect_next = redirect_next + "?{}".format(
request.query_string)
self.auth.flash.set("Invalid credentials")
redirect(
URL(
self.error_page,
vars=dict(next=redirect_next),
use_appname=self.auth.param.use_appname_in_redirects,
)
)


class OAuth2GoogleScoped(object):
"""Class that enables google login via oauth2 with additional scopes.
The authorization info is saved so the scopes can be used later on."""
The authorization info is saved so the scopes can be used later on.
NOTE: if you use this plugin, it is also recommended that you set:
auth.param.auth_enforcer = AuthEnforcerGoogleScoped(auth, error_page="credentials_error")
and that you create a page at URL("credentials_error") to explain the user
that their credentials have expired, and that they must log in again.
This because sometimes, when one tries to use the credentials, Google
complains that the refresh action fails due to missing credentials.
This can happen if the user, or Google, has revoked credentials.
We need to catch this error, and log out the user, so the user
can decide whether they want to login (and create credentials) again.
"""

# These values are used for the plugin registration.
name = "oauth2googlescoped"
Expand Down Expand Up @@ -70,7 +124,7 @@ def _define_tables(self):
self._db.define_table('auth_credentials', [
Field('email'),
Field('name'), # First and last names, all together.
Field('profile_pic'), # URL of profile pic.
Field('profile_pic', 'text'), # URL of profile pic.
Field('credentials', 'text') # Credentials for access, stored in Json for generality.
])

Expand Down

0 comments on commit 397cfab

Please sign in to comment.