From 5b96fcfc2acd21771669c62027aeca693c4da161 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Fri, 6 Feb 2026 14:24:41 -0600 Subject: [PATCH] fix: add support for farmOS/OAuth2 Authorization Code grant/workflow --- src/wuttafarm/web/templates/auth/login.mako | 33 ++++++++ src/wuttafarm/web/views/auth.py | 86 +++++++++++++++++++++ 2 files changed, 119 insertions(+) create mode 100644 src/wuttafarm/web/templates/auth/login.mako diff --git a/src/wuttafarm/web/templates/auth/login.mako b/src/wuttafarm/web/templates/auth/login.mako new file mode 100644 index 0000000..2c71694 --- /dev/null +++ b/src/wuttafarm/web/templates/auth/login.mako @@ -0,0 +1,33 @@ +## -*- coding: utf-8; -*- +<%inherit file="wuttaweb:templates/auth/login.mako" /> +<%namespace name="base_meta" file="/base_meta.mako" /> + +<%def name="page_content()"> +
+ +
+ +
+
+ ${form.render_vue_tag()} +
+
+ +
+ - OR - +
+ +
+
+ +
+
+ +
+
+ diff --git a/src/wuttafarm/web/views/auth.py b/src/wuttafarm/web/views/auth.py index ea3207f..77e9f20 100644 --- a/src/wuttafarm/web/views/auth.py +++ b/src/wuttafarm/web/views/auth.py @@ -23,7 +23,12 @@ Auth views """ +from oauthlib.oauth2 import AccessDeniedError +from requests_oauthlib import OAuth2Session + from wuttaweb.views import auth as base +from wuttaweb.auth import login_user +from wuttaweb.db import Session class AuthView(base.AuthView): @@ -47,6 +52,87 @@ class AuthView(base.AuthView): return None + def get_farmos_oauth2_session(self): + return OAuth2Session( + client_id="farm", + scope="farm_manager", + redirect_uri=self.request.route_url("farmos_oauth_callback"), + ) + + def farmos_oauth_login(self): + """ + View to initiate OAuth2 workflow. + """ + oauth = self.get_farmos_oauth2_session() + auth_url, state = oauth.authorization_url( + self.app.get_farmos_url("/oauth/authorize") + ) + return self.redirect(auth_url) + + def farmos_oauth_callback(self): + """ + View for OAuth2 workflow, provided as redirect URL when + authorizing. + """ + session = Session() + auth = self.app.get_auth_handler() + oauth = self.get_farmos_oauth2_session() + + try: + # get oauth token from farmOS + token = oauth.fetch_token( + self.app.get_farmos_url("/oauth/token"), + authorization_response=self.request.current_route_url(), + include_client_id=True, + ) + + except AccessDeniedError: + self.request.session.flash("Access to farmOS was denied.", "error") + return self.redirect(self.request.route_url("login")) + + # save token in user session + self.request.session["farmos.oauth2.token"] = token + + # get (or create) native app user + farmos_client = self.app.get_farmos_client(token=token) + info = farmos_client.info() + farmos_user = farmos_client.resource.get_id( + "user", "user", info["meta"]["links"]["me"]["meta"]["id"] + ) + user = auth.get_or_make_farmos_user( + session, farmos_user["data"]["attributes"]["name"] + ) + if not user: + self.request.session.flash( + "farmOS authentication was successful, but user is " + "not allowed login to {self.app.get_node_title()}.", + "error", + ) + return self.redirect(self.request.route_url("login")) + + # delare user is logged in + headers = login_user(self.request, user) + referrer = self.request.get_referrer() + return self.redirect(referrer, headers=headers) + + @classmethod + def defaults(cls, config): + cls._auth_defaults(config) + cls._wuttafarm_defaults(config) + + @classmethod + def _wuttafarm_defaults(cls, config): + + # farmos oauth login + config.add_route("farmos_oauth_login", "/farmos/oauth/login") + config.add_view(cls, attr="farmos_oauth_login", route_name="farmos_oauth_login") + + # farmos oauth callback + config.add_route("farmos_oauth_callback", "/farmos/oauth/callback") + config.add_view( + cls, attr="farmos_oauth_callback", route_name="farmos_oauth_callback" + ) + def defaults(config, **kwargs): local = globals()