From d3e1a4412cbc929249e407ccecfb54fbbd14e53a Mon Sep 17 00:00:00 2001 From: Piotr Dobrowolski Date: Wed, 15 Sep 2021 22:43:18 +0200 Subject: [PATCH] authlib rewrite --- README.md | 2 + default.nix | 13 +++++ example.py | 40 ++++++++------ setup.py | 54 ++++++++++--------- spaceauth/__init__.py | 121 ++++++++++++++++++++++++++---------------- 5 files changed, 144 insertions(+), 86 deletions(-) create mode 100644 default.nix diff --git a/README.md b/README.md index a25f001..7da29cd 100644 --- a/README.md +++ b/README.md @@ -4,3 +4,5 @@ Flask-SpaceAuth Simple wrapper around Flask-OAuthlib & Flask-Login for quick and dirty integration of random services with [Warsaw Hackerspace Single Sign-On](https://sso.hackerspace.pl). + +See example in `example.py`. diff --git a/default.nix b/default.nix new file mode 100644 index 0000000..01cb362 --- /dev/null +++ b/default.nix @@ -0,0 +1,13 @@ +{ pkgs ? import {} }: +with pkgs.python3Packages; +buildPythonPackage rec { + pname = "flask-spaceauth-${version}"; + version = "0.3"; + src = ./.; + propagatedBuildInputs = [ + flask + flask_login + blinker + authlib + ]; +} diff --git a/example.py b/example.py index 2df1260..2d6aa3b 100644 --- a/example.py +++ b/example.py @@ -1,26 +1,36 @@ -from flask import Flask, request, url_for, Markup -from spaceauth import SpaceAuth, login_required, cap_required +from flask import Flask, request, url_for, Markup, get_flashed_messages +from spaceauth import SpaceAuth, login_required, cap_required, current_user -app = Flask('spaceauth-example') -app.config['SECRET_KEY'] = 'testing' -app.config['SPACEAUTH_CONSUMER_KEY'] = 'testing' -app.config['SPACEAUTH_CONSUMER_SECRET'] = 'asdTasdfhwqweryrewegfdsfJIxkGc' +app = Flask("spaceauth-example") +app.config["SECRET_KEY"] = "testing" +app.config["SPACEAUTH_CONSUMER_KEY"] = "17817145-de34-4547-b067-64632f04156a" +app.config["SPACEAUTH_CONSUMER_SECRET"] = "SbZGSw8UgV9uWXvQzxn10czBBTLpE7" auth = SpaceAuth(app) -@app.route('/') -def index(): - return Markup('Hey! Login with spaceauth / %r') % ( - url_for('spaceauth.login'), spaceauth.current_user) -@app.route('/profile') +@app.route("/") +def index(): + return Markup( + '
%r
Hey! Login with spaceauth / Logout / %r / Members only space' + ) % ( + get_flashed_messages(), + url_for("spaceauth.login"), + url_for("spaceauth.logout"), + current_user.get_id(), + url_for("profile"), + ) + + +@app.route("/profile") @login_required def profile(): - return Markup('Hey {}!').format(spaceauth.current_user) + return Markup("Hey {}!").format(current_user.get_id()) -@app.route('/staff') -@cap_required('staff') + +@app.route("/staff") +@cap_required("staff") def staff_only(): - return 'This is staff-only zone!' + return "This is staff-only zone!" if __name__ == "__main__": diff --git a/setup.py b/setup.py index 06e11c4..0c3faff 100644 --- a/setup.py +++ b/setup.py @@ -1,36 +1,42 @@ -''' +""" Flask-SpaceAuth --------------- Simple generic user authentication module using `Warsaw Hackerspace SSO Service `_. -Integrates with Flask-Login and Flask-OAuthlib -''' +Integrates with Flask-Login and authlib +""" from setuptools import setup setup( - name='Flask-SpaceAuth', - version='0.2.0', - description='Warsaw Hackerspace SSO Flask module', + name="Flask-SpaceAuth", + version="0.3.0", + description="Warsaw Hackerspace SSO Flask module", long_description=__doc__, - url='https://code.hackerspace.pl/informatic/flask-spaceauth', - author='Piotr Dobrowolski', - author_email='informatic@hackerspace.pl', - license='MIT', + url="https://code.hackerspace.pl/informatic/flask-spaceauth", + author="Piotr Dobrowolski", + author_email="informatic@hackerspace.pl", + license="MIT", classifiers=[ - 'Development Status :: 4 - Beta', - 'Environment :: Web Environment', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: MIT License', - 'Operating System :: OS Independent', - 'Programming Language :: Python', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.4', - 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', - 'Topic :: Software Development :: Libraries :: Python Modules' + "Development Status :: 4 - Beta", + "Environment :: Web Environment", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 2", + "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.4", + "Topic :: Internet :: WWW/HTTP :: Dynamic Content", + "Topic :: Software Development :: Libraries :: Python Modules", + ], + packages=["spaceauth"], + install_requires=[ + "Flask", + "authlib>=0.14", + "Flask-Login>=0.4", + "requests>=2.0", + "blinker", ], - packages=['spaceauth'], - install_requires=['Flask-OAuthlib>=0.9.4', 'Flask-Login>=0.4', 'requests>=2.0', 'blinker'], ) diff --git a/spaceauth/__init__.py b/spaceauth/__init__.py index 932109b..e66a2c3 100644 --- a/spaceauth/__init__.py +++ b/spaceauth/__init__.py @@ -1,73 +1,91 @@ -from flask import Blueprint, request, url_for, session, redirect, abort, flash -from flask_oauthlib.client import OAuth, OAuthException -from flask_login import LoginManager, login_user, logout_user, current_user, login_required, UserMixin +from flask import Blueprint, request, url_for, session, redirect, flash +from authlib.common.errors import AuthlibBaseError +from authlib.integrations.flask_client import OAuth, OAuthError +from flask_login import ( + LoginManager, + login_user, + logout_user, + current_user, + login_required, + UserMixin, +) from spaceauth.caps import cap_required +from datetime import datetime, timedelta + + +class SSOUser(UserMixin): + def __init__(self, parent, id): + self.id = id + + def __str__(self): + return self.id class SpaceAuth(LoginManager): - def __init__(self, app=None, *args, **kwargs): + def __init__(self, app=None, domain="https://sso.hackerspace.pl", *args, **kwargs): self.oauth = OAuth() - self.remote = self.oauth.remote_app( - 'spaceauth', - base_url='https://sso.hackerspace.pl/api/', - access_token_url='https://sso.hackerspace.pl/oauth/token', - authorize_url='https://sso.hackerspace.pl/oauth/authorize', - request_token_params={'scope': 'profile:read'}, - app_key='SPACEAUTH') - self.remote.tokengetter(self.tokengetter) + self.domain = domain - bp = Blueprint('spaceauth', __name__) - bp.add_url_rule('/login', 'login', self.login_view_handler) - bp.add_url_rule('/logout', 'logout', self.logout_view_handler) - bp.add_url_rule('/callback', 'callback', self.callback_view_handler) + bp = Blueprint("spaceauth", __name__) + bp.add_url_rule("/login", "login", self.login_view_handler) + bp.add_url_rule("/logout", "logout", self.logout_view_handler) + bp.add_url_rule("/callback", "callback", self.callback_view_handler) self.blueprint = bp super(SpaceAuth, self).__init__() - self.refresh_view = 'spaceauth.login' - self.login_view = 'spaceauth.login' + self.refresh_view = "spaceauth.login" + self.login_view = "spaceauth.login" self.user_loader(self.user_loader_handler) if app: self.init_app(app, *args, **kwargs) - def init_app(self, app, url_prefix='/oauth'): + def init_app(self, app, url_prefix="/oauth"): + app.config.get("SPACEAUTH_CONSUMER_KEY") + self.remote = self.oauth.register( + "spaceauth", + client_id=app.config["SPACEAUTH_CONSUMER_KEY"], + client_secret=app.config["SPACEAUTH_CONSUMER_SECRET"], + api_base_url=self.domain + "/api/", + access_token_url=self.domain + "/oauth/token", + authorize_url=self.domain + "/oauth/authorize", + token_endpoint_auth_method="client_secret_post", + # server_metadata_url=self.domain + "/.well-known/openid-configuration", + scope="profile:read", + fetch_token=self.fetch_token, + update_token=self.update_token, + ) self.oauth.init_app(app) super(SpaceAuth, self).init_app(app) app.register_blueprint(self.blueprint, url_prefix=url_prefix) - @app.errorhandler(OAuthException) + @app.errorhandler(AuthlibBaseError) def errorhandler(err): - flash('OAuth error occured', 'error') - return redirect('/') + flash("OAuth error occured ({})".format(err), "error") + return redirect("/") def login_view_handler(self): - session['spaceauth_next'] = request.args.get('next') or request.referrer - return self.remote.authorize( - callback=url_for('spaceauth.callback', _external=True) - ) + session["spaceauth_next"] = request.args.get("next") or request.referrer + return self.remote.authorize_redirect( + redirect_uri=url_for("spaceauth.callback", _external=True) + ) def logout_view_handler(self): # TODO revoke token - session.pop('spaceauth_token', None) - session.pop('spaceauth_next', None) + session.pop("spaceauth_token", None) + session.pop("spaceauth_next", None) logout_user() - return redirect('/') + return redirect("/") def callback_view_handler(self): - resp = self.remote.authorized_response() - if resp is None: - raise OAuthException( - 'Access denied', type=request.args.get('error')) - - # TODO encrypt token...? - session['spaceauth_token'] = resp['access_token'] - profile = self.remote.get('profile').data - - login_user(self.user_loader_handler(profile['username'], profile)) - return redirect(session.pop('spaceauth_next', None) or '/') - - def tokengetter(self): - return (session.get('spaceauth_token'), '') + try: + resp = self.remote.authorize_access_token() + self.update_token(resp) + return redirect(session.pop("spaceauth_next", None) or "/") + except Exception as exc: + raise OAuthError( + error="Unable to authorize access token", description=str(exc) + ) def user_loader_handler(self, uid, profile=None): """ @@ -75,9 +93,18 @@ class SpaceAuth(LoginManager): anonymous. """ - user = UserMixin() - user.id = uid - return user + return SSOUser(self, uid) def user_profile(self): - return self.remote.get('profile').data + return self.remote.get("profile").data + + def fetch_token(self): + return session.get("spaceauth_token") + + def update_token(self, token, refresh_token=None, access_token=None): + session["spaceauth_token"] = token + profile = self.remote.get("1/userinfo").json() + login_user( + self.user_loader_handler(profile["sub"], profile), + duration=timedelta(seconds=token["expires_in"]), + )