Auth redirects for service when making request from JS

Hello,

We are using a JupyterHub Announcement service that is a fork of this repo. We made few changes to the original repo and most important one is put the latest announcement handler under authenticated decorator. It was working fine with JupyterHub 3 and 4 until recent hardening on authentication.

As documented, the latest announcement is fetched from jquery GET request to show it on the hub pages. With JupyterHub 4.1.5, it results in auth redirects and eventually giving up:

[D 2024-04-10 12:00:04.667 AnnouncementService auth:521] HubAuth cache miss: token:db4396dd5e994771b4c0b50d21c79b04:c28a3de10457a9e98f152ef0917a39c05acd08d2cc30a00b7003b0098810a1ae
[D 2024-04-10 12:00:04.704 AnnouncementService auth:532] Received request from Hub user {'name': 'paipuri', 'admin': True, 'groups': [], 'kind': 'user', 'session_id': 'db4396dd5e994771b4c0b50d21c79b04', 'scopes': ['access:services!service=announcement', 'read:users:groups!user=paipuri', 'read:users:name!user=paipuri']}
[D 2024-04-10 12:00:04.705 AnnouncementService auth:1305] Allowing Hub user paipuri (all Hub users and services allowed)
[D 2024-04-10 12:00:04.705 AnnouncementService _xsrf_utils:137] xsrf id mismatch b'db4396dd5e994771b4c0b50d21c79b04:eb67dd829adef65f38cffd80b70448a866f4760db6145aebe8d93fbe025f30df' != b'db4396dd5e994771b4c0b50d21c79b04:c28a3de10457a9e98f152ef0917a39c05acd08d2cc30a00b7003b0098810a1ae'
[I 2024-04-10 12:00:04.705 AnnouncementService _xsrf_utils:159] Setting new xsrf cookie for b'db4396dd5e994771b4c0b50d21c79b04:c28a3de10457a9e98f152ef0917a39c05acd08d2cc30a00b7003b0098810a1ae' {'path': '/services/announcement/'}
[I 2024-04-10 12:00:04.719 AnnouncementService web:2348] 200 GET /services/announcement/ (127.0.0.1) 59.16ms
[I 2024-04-10 12:00:04.748 AnnouncementService web:2348] 200 GET /services/announcement/static/css/style.min.css?v=01598a5386176f0279952a3b9632a07e7fce9a12aa53108973c83be9ec3473e7a59354876fab64bfeb01892eb503870183707aa03f207d7a94845ca7980c3819 (127.0.0.1) 4.92ms
[I 2024-04-10 12:00:04.750 AnnouncementService web:2348] 200 GET /services/announcement/static/components/requirejs/require.js?v=bd1aa102bdb0b27fbf712b32cfcd29b016c272acf3d864ee8469376eaddd032cadcf827ff17c05a8c8e20061418fe58cf79947049f5c0dff3b4f73fcc8cad8ec (127.0.0.1) 0.76ms
[I 2024-04-10 12:00:04.752 AnnouncementService web:2348] 200 GET /services/announcement/static/components/jquery/dist/jquery.min.js?v=bf6089ed4698cb8270a8b0c8ad9508ff886a7a842278e98064d5c1790ca3a36d5d69d9f047ef196882554fc104da2c88eb5395f1ee8cf0f3f6ff8869408350fe (127.0.0.1) 0.68ms
[I 2024-04-10 12:00:04.753 AnnouncementService web:2348] 200 GET /services/announcement/static/components/bootstrap/dist/js/bootstrap.min.js?v=a014e9acc78d10a0a7a9fbaa29deac6ef17398542d9574b77b40bf446155d210fa43384757e3837da41b025998ebfab4b9b6f094033f9c226392b800df068bce (127.0.0.1) 0.72ms
[D 2024-04-10 12:00:04.782 AnnouncementService auth:776] No user identified
[D 2024-04-10 12:00:04.782 AnnouncementService auth:1277] Redirecting to login url: /hub/api/oauth2/authorize?client_id=service-announcement&redirect_uri=%2Fservices%2Fannouncement%2Foauth_callback&response_type=code
[W 2024-04-10 12:00:04.783 AnnouncementService auth:1087] Detected unused OAuth state cookies
[I 2024-04-10 12:00:04.783 AnnouncementService web:2348] 302 GET /services/announcement/latest (127.0.0.1) 2.11ms
[I 2024-04-10 12:00:04.789 AnnouncementService web:2348] 200 GET /services/announcement/static/components/font-awesome/fonts/fontawesome-webfont.woff2?v=4.7.0 (127.0.0.1) 1.40ms
[D 2024-04-10 12:00:04.917 AnnouncementService auth:521] HubAuth cache miss: token:db4396dd5e994771b4c0b50d21c79b04:07781bf94beb8ce8a18941c7860042b99dab66e0550c9077bb5dac5b77701f60
[D 2024-04-10 12:00:04.942 AnnouncementService auth:532] Received request from Hub user {'name': 'paipuri', 'admin': True, 'groups': [], 'kind': 'user', 'session_id': 'db4396dd5e994771b4c0b50d21c79b04', 'scopes': ['access:services!service=announcement', 'read:users:groups!user=paipuri', 'read:users:name!user=paipuri']}
[I 2024-04-10 12:00:04.942 AnnouncementService auth:1481] Logged-in user {'name': 'paipuri', 'admin': True, 'groups': [], 'kind': 'user', 'session_id': 'db4396dd5e994771b4c0b50d21c79b04', 'scopes': ['access:services!service=announcement', 'read:users:groups!user=paipuri', 'read:users:name!user=paipuri']}
[D 2024-04-10 12:00:04.942 AnnouncementService auth:1156] Setting oauth cookie for 127.0.0.1: service-announcement, {'path': '/services/announcement/', 'httponly': True}
[I 2024-04-10 12:00:04.942 AnnouncementService web:2348] 302 GET /services/announcement/oauth_callback?code=DiFaL6wPpwNs0GS5b0cOjnLa6JrSsY&state=eyJ1dWlkIjogIjU0ZTc4ZDJjNmIyMDRmYjc4MTgyYjliYTBiN2I0MzY2IiwgIm5leHRfdXJsIjogIi9zZXJ2aWNlcy9hbm5vdW5jZW1lbnQvbGF0ZXN0IiwgImNvb2tpZV9uYW1lIjogInNlcnZpY2UtYW5ub3VuY2VtZW50LW9hdXRoLXN0YXRlLWhvVVp6YVB5In0 (127.0.0.1) 95.19ms
[D 2024-04-10 12:00:04.950 AnnouncementService auth:776] No user identified
[D 2024-04-10 12:00:04.950 AnnouncementService auth:1277] Redirecting to login url: /hub/api/oauth2/authorize?client_id=service-announcement&redirect_uri=%2Fservices%2Fannouncement%2Foauth_callback&response_type=code
[W 2024-04-10 12:00:04.950 AnnouncementService auth:1087] Detected unused OAuth state cookies
[I 2024-04-10 12:00:04.951 AnnouncementService web:2348] 302 GET /services/announcement/latest (127.0.0.1) 1.53ms
[D 2024-04-10 12:00:05.053 AnnouncementService auth:521] HubAuth cache miss: token:db4396dd5e994771b4c0b50d21c79b04:f0bd62aef582917f3502544a0d4b0141e4ae03a825e380a7e85c10229d9f04e9
[D 2024-04-10 12:00:05.087 AnnouncementService auth:532] Received request from Hub user {'name': 'paipuri', 'admin': True, 'groups': [], 'kind': 'user', 'session_id': 'db4396dd5e994771b4c0b50d21c79b04', 'scopes': ['access:services!service=announcement', 'read:users:groups!user=paipuri', 'read:users:name!user=paipuri']}
[I 2024-04-10 12:00:05.087 AnnouncementService auth:1481] Logged-in user {'name': 'paipuri', 'admin': True, 'groups': [], 'kind': 'user', 'session_id': 'db4396dd5e994771b4c0b50d21c79b04', 'scopes': ['access:services!service=announcement', 'read:users:groups!user=paipuri', 'read:users:name!user=paipuri']}
[D 2024-04-10 12:00:05.087 AnnouncementService auth:1156] Setting oauth cookie for 127.0.0.1: service-announcement, {'path': '/services/announcement/', 'httponly': True}
[I 2024-04-10 12:00:05.088 AnnouncementService web:2348] 302 GET /services/announcement/oauth_callback?code=VJbcMxguc1ntOHb1SMIT0rAdXUEhe1&state=eyJ1dWlkIjogIjYwZjFhNjZkNTNmMjRmMTE4NTY1NzE1ZTdmYWRkZGIyIiwgIm5leHRfdXJsIjogIi9zZXJ2aWNlcy9hbm5vdW5jZW1lbnQvbGF0ZXN0IiwgImNvb2tpZV9uYW1lIjogInNlcnZpY2UtYW5ub3VuY2VtZW50LW9hdXRoLXN0YXRlLVBjaG9mUVNGIn0 (127.0.0.1) 76.06ms
[D 2024-04-10 12:00:05.098 AnnouncementService auth:776] No user identified
[D 2024-04-10 12:00:05.098 AnnouncementService auth:1277] Redirecting to login url: /hub/api/oauth2/authorize?client_id=service-announcement&redirect_uri=%2Fservices%2Fannouncement%2Foauth_callback&response_type=code
[W 2024-04-10 12:00:05.098 AnnouncementService auth:1087] Detected unused OAuth state cookies
[I 2024-04-10 12:00:05.099 AnnouncementService web:2348] 302 GET /services/announcement/latest (127.0.0.1) 2.07ms
[D 2024-04-10 12:00:05.213 AnnouncementService auth:521] HubAuth cache miss: token:db4396dd5e994771b4c0b50d21c79b04:74f84740c2e56f01fe5fa13f1b62d0b3c51fdcbb8376ca857ac07b953e227b95
[D 2024-04-10 12:00:05.247 AnnouncementService auth:532] Received request from Hub user {'name': 'paipuri', 'admin': True, 'groups': [], 'kind': 'user', 'session_id': 'db4396dd5e994771b4c0b50d21c79b04', 'scopes': ['access:services!service=announcement', 'read:users:groups!user=paipuri', 'read:users:name!user=paipuri']}
[I 2024-04-10 12:00:05.247 AnnouncementService auth:1481] Logged-in user {'name': 'paipuri', 'admin': True, 'groups': [], 'kind': 'user', 'session_id': 'db4396dd5e994771b4c0b50d21c79b04', 'scopes': ['access:services!service=announcement', 'read:users:groups!user=paipuri', 'read:users:name!user=paipuri']}
[D 2024-04-10 12:00:05.247 AnnouncementService auth:1156] Setting oauth cookie for 127.0.0.1: service-announcement, {'path': '/services/announcement/', 'httponly': True}
[I 2024-04-10 12:00:05.248 AnnouncementService web:2348] 302 GET /services/announcement/oauth_callback?code=Ep0AnWCs568PxQYepc6ZzS5l20Mdig&state=eyJ1dWlkIjogImZkNjE2NWVlZTFiZjQxMDQ4NWM5ZGEwYTE3ZDYwNDZjIiwgIm5leHRfdXJsIjogIi9zZXJ2aWNlcy9hbm5vdW5jZW1lbnQvbGF0ZXN0IiwgImNvb2tpZV9uYW1lIjogInNlcnZpY2UtYW5ub3VuY2VtZW50LW9hdXRoLXN0YXRlLU1uelVlUGdOIn0 (127.0.0.1) 107.58ms
[I 2024-04-10 12:00:05.267 AnnouncementService web:2348] 200 GET /services/announcement/static/favicon.ico?v=fde5757cd3892b979919d3b1faa88a410f28829feb5ba22b6cf069f2c6c98675fceef90f932e49b510e74d65c681d5846b943e7f7cc1b41867422f0481085c1f (127.0.0.1) 2.17ms
[D 2024-04-10 12:00:05.274 AnnouncementService auth:776] No user identified
[D 2024-04-10 12:00:05.275 AnnouncementService auth:1277] Redirecting to login url: /hub/api/oauth2/authorize?client_id=service-announcement&redirect_uri=%2Fservices%2Fannouncement%2Foauth_callback&response_type=code
[W 2024-04-10 12:00:05.275 AnnouncementService auth:1087] Detected unused OAuth state cookies
[I 2024-04-10 12:00:05.276 AnnouncementService web:2348] 302 GET /services/announcement/latest (127.0.0.1) 3.31ms
[D 2024-04-10 12:00:05.387 AnnouncementService auth:521] HubAuth cache miss: token:db4396dd5e994771b4c0b50d21c79b04:0eb4707d0d9292b74474c23b33965035f663f51c365dc8a184e4be8277e675aa
[D 2024-04-10 12:00:05.414 AnnouncementService auth:532] Received request from Hub user {'name': 'paipuri', 'admin': True, 'groups': [], 'kind': 'user', 'session_id': 'db4396dd5e994771b4c0b50d21c79b04', 'scopes': ['access:services!service=announcement', 'read:users:groups!user=paipuri', 'read:users:name!user=paipuri']}
[I 2024-04-10 12:00:05.414 AnnouncementService auth:1481] Logged-in user {'name': 'paipuri', 'admin': True, 'groups': [], 'kind': 'user', 'session_id': 'db4396dd5e994771b4c0b50d21c79b04', 'scopes': ['access:services!service=announcement', 'read:users:groups!user=paipuri', 'read:users:name!user=paipuri']}
[D 2024-04-10 12:00:05.414 AnnouncementService auth:1156] Setting oauth cookie for 127.0.0.1: service-announcement, {'path': '/services/announcement/', 'httponly': True}
[I 2024-04-10 12:00:05.415 AnnouncementService web:2348] 302 GET /services/announcement/oauth_callback?code=VWqYrfwkkZqgeSDGRjgyoEzKtUA7gh&state=eyJ1dWlkIjogImE5NmYxYmZjNTgzODQ2NzliNzI4NDU1MGE2YWQ5N2Q0IiwgIm5leHRfdXJsIjogIi9zZXJ2aWNlcy9hbm5vdW5jZW1lbnQvbGF0ZXN0IiwgImNvb2tpZV9uYW1lIjogInNlcnZpY2UtYW5ub3VuY2VtZW50LW9hdXRoLXN0YXRlLXpOdG1sZ0ZwIn0 (127.0.0.1) 72.94ms
[D 2024-04-10 12:00:05.482 AnnouncementService auth:776] No user identified
[D 2024-04-10 12:00:05.482 AnnouncementService auth:1277] Redirecting to login url: /hub/api/oauth2/authorize?client_id=service-announcement&redirect_uri=%2Fservices%2Fannouncement%2Foauth_callback&response_type=code
[W 2024-04-10 12:00:05.482 AnnouncementService auth:1087] Detected unused OAuth state cookies
[I 2024-04-10 12:00:05.483 AnnouncementService web:2348] 302 GET /services/announcement/latest (127.0.0.1) 2.03ms
[D 2024-04-10 12:00:05.553 AnnouncementService auth:521] HubAuth cache miss: token:db4396dd5e994771b4c0b50d21c79b04:efb50fbd81ec7531165e96d50a3a2fc826372ced70662ead626f842af5237860
[D 2024-04-10 12:00:05.582 AnnouncementService auth:532] Received request from Hub user {'name': 'paipuri', 'admin': True, 'groups': [], 'kind': 'user', 'session_id': 'db4396dd5e994771b4c0b50d21c79b04', 'scopes': ['access:services!service=announcement', 'read:users:groups!user=paipuri', 'read:users:name!user=paipuri']}
[I 2024-04-10 12:00:05.582 AnnouncementService auth:1481] Logged-in user {'name': 'paipuri', 'admin': True, 'groups': [], 'kind': 'user', 'session_id': 'db4396dd5e994771b4c0b50d21c79b04', 'scopes': ['access:services!service=announcement', 'read:users:groups!user=paipuri', 'read:users:name!user=paipuri']}
[D 2024-04-10 12:00:05.582 AnnouncementService auth:1156] Setting oauth cookie for 127.0.0.1: service-announcement, {'path': '/services/announcement/', 'httponly': True}
[I 2024-04-10 12:00:05.583 AnnouncementService web:2348] 302 GET /services/announcement/oauth_callback?code=hJzZKu5dHjQVFY2UTxYjrNlAAuId5C&state=eyJ1dWlkIjogIjIzNGM5OTI2YmY0ODQ0NWZhOThjN2I2MGFkZTM4Y2ZlIiwgIm5leHRfdXJsIjogIi9zZXJ2aWNlcy9hbm5vdW5jZW1lbnQvbGF0ZXN0IiwgImNvb2tpZV9uYW1lIjogInNlcnZpY2UtYW5ub3VuY2VtZW50LW9hdXRoLXN0YXRlLVhlaktJTklxIn0 (127.0.0.1) 70.29ms
[D 2024-04-10 12:00:05.596 AnnouncementService auth:776] No user identified
[D 2024-04-10 12:00:05.596 AnnouncementService auth:1277] Redirecting to login url: /hub/api/oauth2/authorize?client_id=service-announcement&redirect_uri=%2Fservices%2Fannouncement%2Foauth_callback&response_type=code
[W 2024-04-10 12:00:05.596 AnnouncementService auth:1087] Detected unused OAuth state cookies

Is it not possible to make requests to authenticated endpoints of services from javascript? Is there any other way to achieve what we are trying here.

Cheers

Here is a minimum reproducer based on the announcement example in JupyterHub repo.

Announcement service which swaps HubAuthenticated for HubOAuthenticated and add other necessary handlers:

import argparse
import datetime
import json
import os

from tornado import escape, ioloop, web

from jupyterhub.services.auth import HubOAuthenticated, HubOAuthCallbackHandler


class AnnouncementRequestHandler(HubOAuthenticated, web.RequestHandler):
    """Dynamically manage page announcements"""

    def initialize(self, storage):
        """Create storage for announcement text"""
        self.storage = storage

    @web.authenticated
    def post(self):
        """Update announcement"""
        user = self.get_current_user()
        doc = escape.json_decode(self.request.body)
        self.storage["announcement"] = doc["announcement"]
        self.storage["timestamp"] = datetime.datetime.now().isoformat()
        self.storage["user"] = user["name"]
        self.write_to_json(self.storage)

    @web.authenticated
    def get(self):
        """Retrieve announcement"""
        self.write_to_json(self.storage)

    @web.authenticated
    def delete(self):
        """Clear announcement"""
        self.storage["announcement"] = ""
        self.write_to_json(self.storage)

    def write_to_json(self, doc):
        """Write dictionary document as JSON"""
        self.set_header("Content-Type", "application/json; charset=UTF-8")
        self.write(escape.utf8(json.dumps(doc)))


def main():
    args = parse_arguments()
    application = create_application(**vars(args))
    application.listen(args.port)
    ioloop.IOLoop.current().start()


def parse_arguments():
    parser = argparse.ArgumentParser()
    parser.add_argument(
        "--api-prefix",
        "-a",
        default=os.environ.get("JUPYTERHUB_SERVICE_PREFIX", "/"),
        help="application API prefix",
    )
    parser.add_argument(
        "--port", "-p", default=8888, help="port for API to listen on", type=int
    )
    return parser.parse_args()


def create_application(api_prefix="/", handler=AnnouncementRequestHandler, **kwargs):
    storage = dict(announcement="test", timestamp="", user="admin")
    settings = {
        "cookie_secret": "secret",
    }
    return web.Application([(api_prefix, handler, dict(storage=storage)), (f"{api_prefix}oauth_callback", HubOAuthCallbackHandler)], **settings)


if __name__ == "__main__":
    main()

Modified JupyterHub config:

import sys

c = get_config()  # noqa

# To run the announcement service managed by the hub, add this.

port = 9999
c.JupyterHub.services = [
    {
        'name': 'announcement',
        'url': f'http://127.0.0.1:{port}',
        'command': [
            sys.executable,
            "-m",
            "announcement",
            "--api-prefix",
            "/services/announcement/",
            '--port',
            str(port),
        ],
        'oauth_no_confirm': True,
    }
]

# The announcements need to get on the templates somehow, see page.html
# for an example of how to do this.

c.JupyterHub.template_paths = ["templates"]

c.Authenticator.allowed_users = {"announcer", "otheruser"}

# grant the 'announcer' permission to access the announcement service
c.JupyterHub.load_roles = [
    {
        "name": "announcers",
        "users": ["announcer"],
        "scopes": ["access:services!service=announcement"],
    }
]

# dummy spawner and authenticator for testing, don't actually use these!
c.JupyterHub.authenticator_class = 'dummy'
c.JupyterHub.spawner_class = 'simple'
c.JupyterHub.ip = '127.0.0.1'  # let's just run on localhost while dummy auth is enabled

Now using the custom template to include jQuery API request to get latest announcement

{% extends "templates/page.html" %} {% block announcement %}
<div class="container text-center announcement"></div>
{% endblock %} {% block script %} {{ super() }}
<script>
  $.get("/services/announcement/", function (data) {
    $(".announcement").html(data["announcement"]);
  });
</script>
{% endblock %}

We can notice there would be a OAuth redirect loops in the browser.

I’ve created a bug report for one of the other service examples ( service-whoami):

Since service-announcement is an official example it should also “just work”, would you mind either adding to the issue or opening a new one?

Cheers @manics !! Well, the official example of service-announcement in the repo works. I have modified the official example as follows:

  • Swap HubAuthenticated for HubOAuthenticated and added HubOAuthCallbackHandler handler.
  • Most importantly, I added web.authenticated decorator for getting the announcement as well. In the official example, GET endpoint is unauthenticated.

Even with HubAuthenticated, once we put GET endpoint under authenticated decorator, these redirect loops happen. So, yes, it is the same issue that you have created. I will leave a comment on the issue.

If you want to allow cross-service requests to be authenticated with cookies, you’ll need to modify or disable the XSRF check. There are two ways:

  1. set disable_check_xsrf=True in the tornado settings of your application. This will allow cross-site requests to your service (probably fine for announcements, but consider what’s in them)
  2. define check_xsrf_cookie in your Handler class, to rely on another check that accepts requests from your whole site:
class MyHandler(HubOAuthenticated, web.RequestHandler):
    def check_xsrf_cookie(self):
        # replace xsrf token with Sec-Fetch requirement
        sec_fetch_site = self.headers.get("Sec-Fetch-Site", "unspecified")
        if sec_fetch_site != "same-origin":
            raise web.HTTPError(403, f"rejecting cross-origin request from: {sec_fetch_site}")

1 Like

Cheers @minrk !! Yes, I ended up disabling xsrf check for announcement service but didnt think about overriding check_xsrf_cookie in the handler. Thanks for the suggestion, I will look into it.