Page MenuHomePhabricator

Define standard JWT session data for supported session types
Closed, ResolvedPublic

Description

For T398815: WE5.1.2 Verifiable MediaWiki sessions, we want to define a standard data structure to put inside session JWTs. We'll probably want to do this in MediaWiki core, so it's easy to use in any session provider.

It could take the form of a utility method like SessionManager::getJwtData(), which would use a hook to allow extensions / sites to add additional data.

Currently, the data contained in Wikimedia OAuth 2 access tokens looks like this:

  • Standard JWT fields
    • iss (issuer) - wiki origin URL
    • sub (subject) - central user ID (the CentralAuth ID for SUL wikis, the local user ID for other wikis)
    • aud (audience) - OAuth app ID
    • jti (JWT ID) - unique ID, which can be used to prevent replay attacks
    • iat (issued at) - timestamp of when the JWT was generated
    • exp (expiration time) - timestamp of token expiry (for OAuth 2 owner-only consumerts, this is 1000 years after issuance; for normal consumers, it's 4 hours after issuance)
    • nbf (not before) - start of validity. Not really used, we set it the same as iat
  • Extra fields
    • scopes - list of permission bundles associated with the session (OAuth 2.0 calls these scopes; OAuth 1.0 and MediaWiki core calls them grants). This is the same data that's used for SessionProvider::getAllowedUserRights() but it's informational (MediaWiki will load it from the DB rather than relying on the JWT)
    • ratelimit - rate limiting information for Envoy, in the form of [ "requests_per_unit" => 1234, "unit" => "HOUR" ]

For the purposes of rate limiting, this is approximately right, but probably could use small improvements:

  • For SUL wikis, the issuer should probably be Meta (the designated OAuth / SUL wiki) rather than the current wiki?
  • We should probably namespace the user ID (so two users at two different non-SUL wikis, or a SUL wiki and a non-SUL wiki, don't end up with the same sub value). The spec says The subject value MUST either be scoped to be locally unique in the context of the issuer or be globally unique. so technically we are not violating it (since the issuer is the wiki URL, and for a given wiki user IDs can't conflict), but for the purposes of rate limiting, globally unique values would probably be nicer? So maybe we should have something like centralauth:<user id> (SUL) / <wiki id>:<user id> (non-SUL).
  • Similarly, we might want to namespace OAuth app IDs. This is trickier as the OIDC spec requires this to be exactly the OAuth client ID, so we'd have to namespace those, which would be very disruptive. It's a multi-value field though, so we could have one value that fulfills the spec, and another that's used internally.
    • Also, since the goal is developer authentication, maybe we'll also want something here that's more descriptive than a client ID, such as the developer's user ID?
  • Should we put something in aud for cookie-based sessions (which aren't associated with an application / developer)? The JWT spec doesn't require the field to always be present, but maybe convenient to put the user ID there?
  • For rate limiting, rather than encoding the specific numbers in the JWT (which than cannot be changed, potentially forever in the case of owner-only consumers), we probably want some sort of rate limit classes?

Details

Related Changes in Gerrit:

Event Timeline

Tgr renamed this task from Create JWT session data for supported session types to Define standard JWT session data for supported session types.Jul 10 2025, 8:18 PM
Tgr updated the task description. (Show Details)
Tgr updated the task description. (Show Details)

Look good to me overall. A couple thoughts:

  • having a human readable user name in the JWT would be useful for logging and eyeballing
  • How will sessions for anonymous users work? What would they use for a user ID?
  • Rate limitclasses would be preferable, since it make it easier for SREs to tweak limits as needed. We can suppor both, but it would add complexity. I'd expect the classes coming from mediawiki to be based on permissions, like the "ApiHighLimits" permission in the action API. We can start with just using that (ApiHighLimits vs ApiDefaultLimits), ew can always make it more fine grained later.

having a human readable user name in the JWT would be useful for logging and eyeballing

On one hand yes, on the other hand it's up to 250 extra bytes in every request. I think I'd not do it, at least initially, so we only introduce it once we have a use case, and so it's easier to measure performance impacts once we introduce it.

How will sessions for anonymous users work? What would they use for a user ID?

Options I can think of: omit the field (in theory all JWT fields are optional), use an empty string, use 0, use something like "anon", use something that's a reasonable approximation of an identifier (most likely, the IP address). "anon" seems the cleanest to me, but rate limiting logic would have to be aware (as we probably don't want to rate-limit all anon requests as if they were all for the same user).

I'd expect the classes coming from mediawiki to be based on permissions

We also have the existing rate limiting classes from the OAuthRateLimiter extension (T392647#10802093), I imagine we'd have to merge the two, at least for OAuth 2 requests.

@Joe I thought the signature would take up way more space, but I tried with some test data and the JWT is around 400 bytes, so I assume that's not that big a concern.

This is what I'm planning to go with for now:

  • iss - Meta origin URL for SUL wikis, otherwise the wiki's own origin
  • sub - anon for anons, centralauth:<global user id> for SUL wikis, <wiki id>:<local user id> otherwise
  • aud - OAuth app ID. Omitted for non-OAuth requests. (Not dealing with namespacing for now, will wait until we have a concrete use case.)
  • jti - a short random string
  • iat - timestamp of when the JWT was generated
  • sxp - soft expiry, the request is treated like an anonymous one for rate limiting purposes after this. Short (hours?), matches the JWT cookie expiry. (This is not the same as the session lifetime, which is typically much much longer, but we can just regularly refresh the cookie.)
  • exp - a timstamp by which the token would have been definitely refreshed (ie. something significantly larger than sxp). The idea being that low-level JWT libraries might reject the JWT if it has expired, and we want the edge to treat expired and invalid (faked) sessions differently.
  • rlc - rate limiting class. To be defined in T399632: Add a rate limiting class to session JWTs.

Change #1171201 had a related patch set uploaded (by Gergő Tisza; author: Gergő Tisza):

[mediawiki/core@master] [WIP] Add SessionManager::getJwtData()

https://gerrit.wikimedia.org/r/1171201

Probably should have some sort of version number in case we change the schema in the future, let's call it jwv.

I think we can call this done.

Probably should have some sort of version number in case we change the schema in the future, let's call it jwv.

I ended up not doing this. Because part of the claims get generated in core via a standard mechanism with a hook system, and part of them are generated by the session provider, it's not obvious which one should own the versioning. I'll just punt on the problem until we actually need to change the format.

Change #1171201 merged by jenkins-bot:

[mediawiki/core@master] Add SessionManager::getJwtData()

https://gerrit.wikimedia.org/r/1171201