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).