Skip to content

Hub API authentication and authorization

How a Hub API caller authenticates, which role each route needs, and how browser writes carry the cross-site request forgery (CSRF) token. Request validation and the site endpoints are in Site management.

Authorization

Authentication

require_login and optional_login are UI-only. API routes use the API authorization decorator instead. It runs an authentication chain and then a role check (core/src/ecmwf/sites/core/auth/authn.py, authz.py):

Handler Credential Identity when accepted
NoAuthHandler none (anonymous routes only) anonymous role
SitesHubMasterTokenHandler platform master token (hub.master_token) the api-administration-token user, with the platform administrator and staff policies
SitesSiteMasterTokenHandler Site master token, on /hub/api/v*/sites/<space>/<site>/… paths only the master-token user, with the Site administrator groups, the platform administrator groups, and the Site access groups, deduplicated
OIDCTokenHandler bearer token in the Authorization header; accepted only when its client belongs to the platform (azp/aud, SEC-32) the provider user and policies
CookieHandler Hub OIDC id-token cookie (signed; the claims are the JWS payload and the expiry is checked) the signed-in browser user

Bearer values in query parameters and form fields are not accepted: the token handlers read only the Authorization header, and the code documents query and form token transport as intentionally ignored. Token comparison for the master-token handlers is constant-time. When hub.master_token is empty, SitesHubMasterTokenHandler accepts no token: an empty expected value never matches. Detailed token semantics and the proxy-side rules are in security design.

Roles

Each API route selects one authorization instance: anonymous (any authenticated identity, or anonymous where the chain allows it), staff, or platform administrator. Unauthenticated calls answer 401 with a WWW-Authenticate: Bearer header; authenticated calls with an insufficient role answer 403. Site-scoped handlers add a third layer inside the handler: site.get_access_management().is_current_user_admin (Site administrators by user and group lists, plus platform administrators), which answers 403.

The authorization layer distinguishes:

  • Site owners and administrators, who manage the Site and its credentials;
  • staff users, who can view content and use permitted staff operations;
  • platform administrators, who can perform platform-wide actions;
  • users and groups selected by Site access settings.

The Site resource supplies the access data used by the Hub and by the Site configuration.

The Hub caches the signed-in user's roles in the session and rebuilds them at most every five minutes (SESSION_CONTEXT_TTL_SECONDS, core/.../auth/view_utils.py). The rebuild reads the identity provider userinfo endpoint with the access token, because the id-token cookie keeps the roles from the login time. A removed role takes effect without a new login. If the userinfo call fails, the Hub keeps the previous roles and tries again on the next request. The anonymous password-grant endpoint (POST /hub/api/v{1,2}/openid-connect/token/) limits the attempts per client IP and per client IP and username (is_rate_limited, Redis, 20 attempts per minute each). The client IP is the address that the proxy middleware resolves, not the X-Forwarded-For header, and the username is trimmed and compared in lower case. The limits slow a credential-stuffing attempt against the identity provider; they do not stop an attempt that uses many client addresses.

CSRF

The Hub enforces a CSRF token for browser writes. A request with an unsafe method (POST, PUT, PATCH, DELETE) that presents an authentication cookie — the Hub session cookie or the OIDC id-token cookie — must also present the session-bound token, in the X-CSRF-Token header or the csrf_token form field; otherwise the Hub answers 403. Bearer requests are exempt: API clients send no cookie, and a cross-site request cannot set the Authorization header. Requests without an authentication cookie are exempt too. The token is generated per session on first use, and compared in constant time (hub/src/ecmwf/sites/hub/csrf.py, registered as a before_request rule). The session cookie itself is named __Host-sites-hub (SESSION_COOKIE_NAME, core/.../config/flask.py); the CSRF exemption keys on the set of authentication cookie names. The __Host- prefix makes the browser accept the cookie only with Secure, Path=/, and no Domain, so a sibling host cannot set or overwrite it. The web interface carries the token automatically: pages render it in a meta tag and a script adds the header to every non-GET jQuery request; HTML forms render a hidden input (hub/tests/test_csrf.py covers the rule).