From 727b3cfad297771beea2a768c8dae02b10a98bfd Mon Sep 17 00:00:00 2001 From: rjp Date: Sun, 8 Dec 2024 13:05:41 +0000 Subject: [PATCH] Add alternate `redirect_uri` support for broken instances Pixelfed instances do not handle OOB OAuth correctly meaning you can't currently login to them using `toot` (e.g. https://github.com/pixelfed/pixelfed/issues/2522 ) This is a fudge to workaround that by allowing you to specify an alternate `redirect_uri` for broken servers which can take the HTTP redirect issued by Pixelfed and let you grab the code. If you don't have a handy HTTP server, you can use `http://localhost` and your browser (at least Safari and Chrome) will have the code in the address bar for copying and pasting into `toot`. --- toot/__init__.py | 1 + toot/api.py | 10 +++++----- toot/auth.py | 14 +++++++------- toot/cli/auth.py | 6 ++++-- toot/config.py | 11 +++++++++-- 5 files changed, 26 insertions(+), 16 deletions(-) diff --git a/toot/__init__.py b/toot/__init__.py index 6ff2323..40613d6 100644 --- a/toot/__init__.py +++ b/toot/__init__.py @@ -17,6 +17,7 @@ class App(NamedTuple): base_url: str client_id: str client_secret: str + redirect_uri: str class User(NamedTuple): diff --git a/toot/api.py b/toot/api.py index a347fd1..4aeb3b6 100644 --- a/toot/api.py +++ b/toot/api.py @@ -77,12 +77,12 @@ def _tag_action(app, user, tag_name, action) -> Response: return http.post(app, user, url) -def create_app(base_url): +def create_app(base_url, redirect_uri): url = f"{base_url}/api/v1/apps" json = { 'client_name': CLIENT_NAME, - 'redirect_uris': 'urn:ietf:wg:oauth:2.0:oob', + 'redirect_uris': redirect_uri, 'scopes': SCOPES, 'website': CLIENT_WEBSITE, } @@ -157,7 +157,7 @@ def fetch_app_token(app): "client_id": app.client_id, "client_secret": app.client_secret, "grant_type": "client_credentials", - "redirect_uri": "urn:ietf:wg:oauth:2.0:oob", + "redirect_uri": app.redirect_uri, "scope": "read write" } @@ -183,7 +183,7 @@ def get_browser_login_url(app: App) -> str: """Returns the URL for manual log in via browser""" return "{}/oauth/authorize/?{}".format(app.base_url, urlencode({ "response_type": "code", - "redirect_uri": "urn:ietf:wg:oauth:2.0:oob", + "redirect_uri": app.redirect_uri, "scope": SCOPES, "client_id": app.client_id, })) @@ -197,7 +197,7 @@ def request_access_token(app: App, authorization_code: str): 'client_id': app.client_id, 'client_secret': app.client_secret, 'code': authorization_code, - 'redirect_uri': 'urn:ietf:wg:oauth:2.0:oob', + 'redirect_uri': app.redirect_uri, } return http.anon_post(url, data=data, allow_redirects=False).json() diff --git a/toot/auth.py b/toot/auth.py index ef84652..c45d67c 100644 --- a/toot/auth.py +++ b/toot/auth.py @@ -12,22 +12,22 @@ def find_instance(base_url: str) -> Instance: raise ConsoleError(f"Instance not found at {base_url}") -def register_app(domain: str, base_url: str) -> App: +def register_app(domain: str, base_url: str, redirect_uri: str) -> App: try: - response = api.create_app(base_url) + response = api.create_app(base_url, redirect_uri) except ApiError: raise ConsoleError("Registration failed.") - app = App(domain, base_url, response['client_id'], response['client_secret']) + app = App(domain, base_url, response['client_id'], response['client_secret'], redirect_uri) config.save_app(app) return app -def get_or_create_app(base_url: str) -> App: +def get_or_create_app(base_url: str, redirect_uri: str) -> App: instance = find_instance(base_url) domain = _get_instance_domain(instance) - return config.load_app(domain) or register_app(domain, base_url) + return config.load_app(domain, redirect_uri) or register_app(domain, base_url, redirect_uri) def create_user(app: App, access_token: str) -> User: @@ -53,8 +53,8 @@ def login_username_password(app: App, email: str, password: str) -> User: def login_auth_code(app: App, authorization_code: str) -> User: try: response = api.request_access_token(app, authorization_code) - except Exception: - raise ConsoleError("Login failed") + except Exception as e: + raise ConsoleError("Login failed", e) return create_user(app, response["access_token"]) diff --git a/toot/cli/auth.py b/toot/cli/auth.py index 0fbc42f..dd2a391 100644 --- a/toot/cli/auth.py +++ b/toot/cli/auth.py @@ -77,9 +77,11 @@ you need to paste here.""".replace("\n", " ") @cli.command() @instance_option -def login(base_url: str): +@click.option("--redirect", "-r", "redirect_uri", help="Redirect URI to use instead of OOB", prompt=True, default="urn:ietf:wg:oauth:2.0:oob") +def login(base_url: str, redirect_uri: str): """Log into an instance using your browser (recommended)""" - app = get_or_create_app(base_url) + app = get_or_create_app(base_url, redirect_uri) + # `redirect_uri` is now stored in `app` for future use / saving. url = api.get_browser_login_url(app) click.echo(click.wrap_text(LOGIN_EXPLANATION)) diff --git a/toot/config.py b/toot/config.py index 1dd2d6f..6075168 100644 --- a/toot/config.py +++ b/toot/config.py @@ -87,10 +87,17 @@ def get_user_app(user_id: str): return extract_user_app(load_config(), user_id) -def load_app(instance: str) -> Optional[App]: +def load_app(instance: str, redirect_uri: str) -> Optional[App]: config = load_config() if instance in config['apps']: - return App(**config['apps'][instance]) + a = App(**config['apps'][instance]) + # Not sure about this bit - if an app was stored without a `redirect_uri`, should + # loading update it to OOB (the previous default) or to the requested `redirect_uri`? + # Stick to OOB for now because presumably if we've saved the app, the login must + # have worked with OOB and there's no need for a `redirect_uri` update. Maybe? + if a.redirect_uri == "": + a.redirect_uri = "urn:ietf:wg:oauth:2.0:oob" + return a def load_user(user_id: str, throw=False):