"Matrixless" federation protocol proposal
semi-WIP, but up for comments
Basic federation proposal
Glossary
- instance - full standalone WorkAdventure deployment, including front, back, pusher services, etc...
- local instance - deployment of WorkAdventure a user has originally authenticated into (currently only anonymous users are suported)
- remote instance - deployment of WorkAdventure other than local instance where user may travel to
- map server - HTTP(s) server hosting Tiled JSON files that describe WorkAdventure rooms
-
instance URL - single base URL where following WorkAdventure endpoints are exposed:
-
/room
- room WebSocket connection -
/token/exchange
- user token exchange -
/.well-known/workadventure-jwks
- instance token signing JWK Set endpoint
-
"Instance" hosts its own frontend, authentication/avatar service (/anonymLogin and /verify) and pusher service
"Map" can be hosted anywhere, as long as it is cors-accessible. People present on a specific map need to use the same pusher service to see each other (or have their pushers share state across federated instances).
Proposal
Pusher service access
In order to exchange current map / player state pusher service
needs to be exposed to all web origins (CORS).
Traveling between instances
When trying to enter a specific map frontend
will determine address of an instance server that is used to host said map based on its URL - ${url.protocol}://${url.host}
should be the default instance URL. When traveling between instances frontend service will be always hosted from local instance URL and only WebSocket connection will be accessing remote instance.
Split between map and instance server
As some people may want to only host a map without a full WorkAdventure instance, a map file can link a specific instance URL that needs to be used instead of an autogenerated one. Instance can be configured to limit specific maps that can be used by inspecting /room?roomId=
WebSocket query parameter. Technical aspects of this part need to be specified further later.
Instance authentication
Note: Currently only "anonymous" instance user authentication is supported, however some proper authentication providers (local username/password database, external OAuth2, LDAP, SAML...) can be implemented. This is out of scope of this proposal.
User authentication ends with frontend receiving a "local" token. This token is used when connecting with local pusher service WebSocket (?token
query parameter). Said token should be treated as a opaque string by a frontend service (for now).
Before opening a new WebSocket connection a frontend service needs to exchange an existing "local" token with "remote" token intended to be used on a specific remote instance. This can be carried out using a HTTP request sent to local instance (pusher service). Said token can be cached (TODO: how long?) in frontend in order to reduce HTTP traffic when traveling between maps.
Internally, mentioned local/remote tokens internally are assymetrically-signed JWT tokens that contain following fields:
-
iss
- string, source instance URL (eg."https://world.hackerspace.pl"
) -
sub
- string, unique instance user ID (eg."123456"
) -
exp
- number, token expiration date -
aud
- string, target instance URL (can be ommited for "local" tokens, needs to be equal to remote instance URL for "remote" tokens) -
name
- string, player name (optional, default tosub
)
A pusher service, when presented with a token needs to:
- check
iss
token field:- if
iss
equals its own instance URL, it needs to verify against a local public key - otherwise it fetches (and caches for 15 minutes) JWK Set at URL
${iss}/.well-known/workadventure-jwks
(??) and uses it to verify the token signature against
- if
- check
aud
token field:- if
aud
exists and does not match current instance URL, token needs to be considered invalid
- if
- check
exp
token field - ifexp
field value is lower than current timestamp, token needs to be considered invalid - when all checks pass,
pusher
service can usename
andsub
values as a profile descriptor to present to other players
Token exchange
"Local" token can be exchanged for "remote" token. Token exchange endpoint accepts following arguments:
-
token
- existing local token-
iss
field in the token needs to be equal to local instance URL (one that is handling the request) -
exp
field needs to be valid (ie. token not expired) -
aud
field needs to be missing or equal toiss
- Token needs to be properly signed by local instance
-
-
instance
- target remote instance URL
Result of that operation is a new token with aud
field set to value of instance
request argument. Additional token fields MAY get changed (eg. when user display name has changed)
Notable workadventure changes to get some basic federation implemented
- Step 0: (this is being tracked in #2)
- Step 1:
- expand JWT token parsing in pusher service (allow externally-signed tokens) (code)
- code seems like "userUUid"/"clientUUid" is only ever passed as a plain string - this needs to be generated based on incoming user sub/iss maybe?
- exchange existing token with local pusher service into one with specified
aud
field and use that to access specific remote instance (prevent impersonation by malicious instance) (code / code)
- expand JWT token parsing in pusher service (allow externally-signed tokens) (code)
- Step 2:
- make pusher use display name extracted from a token instead of name field and display source instance name of a specific user (code)
Possible blockers
-
Need to research how Jitsi embedding works - users on remote instances will use Jitsi configured on a remote instance - does Jitsi frontend verify which origin it is embedded on? Jitsi authentication tokens are handed out by remote instance pusher, so that shouldn't be a problem.