Merge pull request #41 from matrix-org/daniel/threepidinvites-2

Specify third party room invites
pull/977/head
Daniel Wagner-Hall 9 years ago
commit c48abd93b9

@ -66,21 +66,25 @@ paths:
aliases: aliases:
- /join/{roomId} - /join/{roomId}
"/rooms/{roomId}/invite": # With an extra " " to disambiguate from the 3pid invite endpoint
# The extra space makes it sort first for what I'm sure is a good reason.
"/rooms/{roomId}/invite ":
post: post:
summary: Invite a user to participate in a particular room. summary: Invite a user to participate in a particular room.
description: |- description: |-
*Note that there are two forms of this API, which are documented separately.
This version of the API requires that the inviter knows the Matrix
identifier of the invitee.*
This API invites a user to participate in a particular room. This API invites a user to participate in a particular room.
They do not start participating in the room until they actually join the They do not start participating in the room until they actually join the
room. room.
This serves two purposes; firstly, to notify the user that the room
exists (and that their presence is requested). Secondly, some rooms can
only be joined if a user is invited to join it; sending the invite gives
that user permission to join the room.
Only users currently in a particular room can invite other users to Only users currently in a particular room can invite other users to
join that room. join that room.
If the user was invited to the room, the home server will append a
``m.room.member`` event to the room.
security: security:
- accessToken: [] - accessToken: []
parameters: parameters:
@ -91,7 +95,7 @@ paths:
required: true required: true
x-example: "!d41d8cd:matrix.org" x-example: "!d41d8cd:matrix.org"
- in: body - in: body
name: user_id name: body
required: true required: true
schema: schema:
type: object type: object
@ -111,7 +115,109 @@ paths:
application/json: |- application/json: |-
{} {}
schema: schema:
type: object # empty json object type: object
403:
description: |-
You do not have permission to invite the user to the room. A meaningful ``errcode`` and description error text will be returned. Example reasons for rejections are:
- The invitee has been banned from the room.
- The invitee is already a member of the room.
- The inviter is not currently in the room.
- The inviter's power level is insufficient to invite users to the room.
examples:
application/json: |-
{"errcode": "M_FORBIDDEN", "error": "@cheeky_monkey:matrix.org is banned from the room"}
429:
description: This request was rate-limited.
schema:
"$ref": "definitions/error.yaml"
"/rooms/{roomId}/invite":
post:
summary: Invite a user to participate in a particular room.
description: |-
*Note that there are two forms of this API, which are documented separately.
This version of the API does not require that the inviter know the Matrix
identifier of the invitee, and instead relies on third party identifiers.
The homeserver uses an identity server to perform the mapping from
third party identifier to a Matrix identifier.*
This API invites a user to participate in a particular room.
They do not start participating in the room until they actually join the
room.
Only users currently in a particular room can invite other users to
join that room.
If the identity server did know the Matrix user identifier for the
third party identifier, the home server will append a ``m.room.member``
event to the room.
If the identity server does not know a Matrix user identifier for the
passed third party identifier, the homeserver will issue an invitation
which can be accepted upon providing proof of ownership of the third
party identifier. This is achieved by the identity server generating a
token, which it gives to the inviting homeserver. The homeserver will
add an ``m.room.third_party_invite`` event into the graph for the room,
containing that token.
When the invitee binds the invited third party identifier to a Matrix
user ID, the identity server will give the user a list of pending
invitations, each containing:
- The room ID to which they were invited
- The token given to the homeserver
- A signature of the token, signed with the identity server's private key
- The matrix user ID who invited them to the room
If a token is requested from the identity server, the home server will
append a ``m.room.third_party_invite`` event to the room.
security:
- accessToken: []
parameters:
- in: path
type: string
name: roomId
description: The room identifier (not alias) to which to invite the user.
required: true
x-example: "!d41d8cd:matrix.org"
- in: body
name: body
required: true
schema:
type: object
example: |-
{
"id_server": "matrix.org",
"medium": "email",
"address": "cheeky@monkey.com",
"display_name": "A very cheeky monkey"
}
properties:
id_server:
type: string
description: The hostname+port of the identity server which should be used for third party identifier lookups.
medium:
type: string
# TODO: Link to identity service spec when it eixsts
description: The kind of address being passed in the address field, for example ``email``.
address:
type: string
description: The invitee's third party identifier.
display_name:
type: string
description: A user-friendly string describing who has been invited. It should not contain the address of the invitee, to avoid leaking mappings between third party identities and matrix user IDs.
required: ["id_server", "medium", "address", "display_name"]
responses:
200:
description: The user has been invited to join the room.
examples:
application/json: |-
{}
schema:
type: object
403: 403:
description: |- description: |-
You do not have permission to invite the user to the room. A meaningful ``errcode`` and description error text will be returned. Example reasons for rejections are: You do not have permission to invite the user to the room. A meaningful ``errcode`` and description error text will be returned. Example reasons for rejections are:

@ -3,7 +3,14 @@
"content": { "content": {
"membership": "join", "membership": "join",
"avatar_url": "mxc://localhost/SEsfnsuifSDFSSEF#auto", "avatar_url": "mxc://localhost/SEsfnsuifSDFSSEF#auto",
"displayname": "Alice Margatroid" "displayname": "Alice Margatroid",
"third_party_invite": {
"token": "pc98",
"public_key": "abc123",
"key_validity_url": "https://magic.forest/verifykey",
"signature": "q1w2e3",
"sender": "@zun:zun.soft"
}
}, },
"state_key": "@alice:localhost", "state_key": "@alice:localhost",
"origin_server_ts": 1431961217939, "origin_server_ts": 1431961217939,

@ -0,0 +1,14 @@
{
"age": 242352,
"content": {
"display_name": "Alice Margatroid",
"key_validity_url": "https://magic.forest/verifykey",
"public_key": "abc123"
},
"state_key": "pc98",
"origin_server_ts": 1431961217939,
"event_id": "$WLGTSEFSEF:localhost",
"type": "m.room.third_party_invite",
"room_id": "!Cuyf34gef24t:localhost",
"sender": "@example:localhost"
}

@ -1,7 +1,7 @@
{ {
"type": "object", "type": "object",
"title": "The current membership state of a user in the room.", "title": "The current membership state of a user in the room.",
"description": "Adjusts the membership state for a user in a room. It is preferable to use the membership APIs (``/rooms/<room id>/invite`` etc) when performing membership actions rather than adjusting the state directly as there are a restricted set of valid transformations. For example, user A cannot force user B to join a room, and trying to force this state change directly will fail.", "description": "Adjusts the membership state for a user in a room. It is preferable to use the membership APIs (``/rooms/<room id>/invite`` etc) when performing membership actions rather than adjusting the state directly as there are a restricted set of valid transformations. For example, user A cannot force user B to join a room, and trying to force this state change directly will fail. The ``third_party_invite`` property will be set if the invite was an ``m.room.third_party_invite`` event, and absent if the invite was an ``m.room.member`` event.",
"allOf": [{ "allOf": [{
"$ref": "core-event-schema/state_event.json" "$ref": "core-event-schema/state_event.json"
}], }],
@ -21,6 +21,33 @@
"displayname": { "displayname": {
"type": ["string", "null"], "type": ["string", "null"],
"description": "The display name for this user, if any. This is added by the homeserver." "description": "The display name for this user, if any. This is added by the homeserver."
},
"third_party_invite": {
"type": "object",
"title": "invite",
"properties": {
"token": {
"type": "string",
"description": "A token which must be correctly signed, in order to join the room."
},
"key_validity_url": {
"type": "string",
"description": "A URL which can be fetched, with querystring ``public_key=public_key``, to validate whether the key has been revoked. The URL must return a JSON object containing a boolean property named 'valid'."
},
"public_key": {
"type": "string",
"description": "A base64-encoded ed25519 key with which token must be signed."
},
"signature": {
"type": "string",
"description": "A base64-encoded signature of token with public_key."
},
"sender": {
"type": "string",
"description": "The matrix user ID of the user who send the invite which is being used."
}
},
"required": ["token", "key_validity_url", "public_key", "signature", "sender"]
} }
}, },
"required": ["membership"] "required": ["membership"]

@ -0,0 +1,37 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"type": "object",
"title": "An invitation to a room issued to a third party identifier, rather than a matrix user ID.",
"description": "Acts as an ``m.room.member`` invite event, where there isn't a target user_id to invite. This event contains a token and a public key whose private key must be used to sign the token. Any user who can present that signature may use this invitation to join the target room.",
"allOf": [{
"$ref": "core-event-schema/state_event.json"
}],
"properties": {
"content": {
"type": "object",
"properties": {
"display_name": {
"type": "string",
"description": "A user-readable string which represents the user who has been invited. This should not contain the user's third party ID, as otherwise when the invite is accepted it would leak the association between the matrix ID and the third party ID."
},
"key_validity_url": {
"type": "string",
"description": "A URL which can be fetched, with querystring public_key=public_key, to validate whether the key has been revoked. The URL must return a JSON object containing a boolean property named 'valid'."
},
"public_key": {
"type": "string",
"description": "A base64-encoded ed25519 key with which token must be signed."
}
},
"required": ["display_name", "key_validity_url", "public_key"]
},
"state_key": {
"type": "string",
"description": "The token, of which a signature must be produced in order to join the room."
},
"type": {
"type": "string",
"enum": ["m.room.third_party_invite"]
}
}
}

@ -0,0 +1,113 @@
Third party invites
===================
.. _module:third_party_invites:
This module adds in support for inviting new members to a room where their
Matrix user ID is not known, instead addressing them by a third party identifier
such as an email address.
There are two flows here; one if a Matrix user ID is known for the third party
identifier, and one if not. Either way, the client calls ``/invite`` with the
details of the third party identifier.
The homeserver asks the identity server whether a Matrix user ID is known for
that identifier. If it is, an invite is simply issued for that user.
If it is not, the homeserver asks the identity server to record the details of
the invitation, and to notify the client of this pending invitation if it gets
a binding for this identifier in the future. The identity server returns a token
and public key to the homeserver.
If a client then tries to join the room in the future, it will be allowed to if
it presents both the token, and a signature of that token from the identity
server which can be verified with the public key.
Events
------
{{m_room_third_party_invite_event}}
Client behaviour
----------------
A client asks a server to invite a user by their third party identifier.
Server behaviour
----------------
All homeservers MUST verify that sig(``token``, ``public_key``) = ``signature``.
If a client of the current homeserver is joining by an
``m.room.third_party_invite``, that homesever MUST validate that the public
key used for signing is still valid, by checking ``key_validity_url``. It does
this by making an HTTP GET request to ``key_validity_url``:
.. TODO: Link to identity server spec when it exists
Schema::
=> GET $key_validity_url?public_key=$public_key
<= HTTP/1.1 200 OK
{
"valid": true|false
}
Example::
key_validity_url = https://identity.server/is_valid
public_key = ALJWLAFQfqffQHFqFfeqFUOEHf4AIHfefh4
=> GET https://identity.server/is_valid?public_key=ALJWLAFQfqffQHFqFfeqFUOEHf4AIHfefh4
<= HTTP/1.1 200 OK
{
"valid": true
}
with the querystring
?public_key=``public_key``. A JSON object will be returned.
The invitation is valid if the object contains a key named ``valid`` which is
``true``. Otherwise, the invitation MUST be rejected. This request is
idempotent and may be retried by the homeserver.
If a homeserver is joining a room for the first time because of an
``m.room.third_party_invite``, the server which is already participating in the
room (which is chosen as per the standard server-server specification) MUST
validate that the public key used for signing is still valid, by checking
``key_validity_url`` in the above described way.
No other homeservers may reject the joining of the room on the basis of
``key_validity_url``, this is so that all homeservers have a consistent view of
the room. They may, however, indicate to their clients that a member's'
membership is questionable.
For example:
If room R has two participating homeservers, H1, H2
And user A on H1 invites a third party identifier to room R
H1 asks the identity server for a binding to a Matrix user ID, and has none,
so issues an ``m.room.third_party_invite`` event to the room.
When the third party user validates their identity, they are told about the
invite, and ask their homeserver, H3, to join the room.
H3 validates that sign(``token``, ``public_key``) = ``signature``, and may check
``key_validity_url``.
H3 then asks H1 to join it to the room. H1 *must* validate that
sign(``token``, ``public_key``) = ``signature`` *and* check ``key_validity_url``.
Having validated these things, H1 writes the join event to the room, and H3
begins participating in the room. H2 *must* accept this event.
The reason that no other homeserver may reject the event based on checking
``key_validity_url`` is that we must ensure event acceptance is deterministic.
If some other participating server doesn't have a network path to the keyserver,
or if the keyserver were to go offline, or revoke its keys, that other server
would reject the event and cause the participating servers' graphs to diverge.
This relies on participating servers trusting each other, but that trust is
already implied by the server-server protocol. Also, the public key signature
verification must still be performed, so the attack surface here is minimized.

@ -23,6 +23,7 @@ groups: # reusable blobs of files when prefixed with 'group:'
- modules/end_to_end_encryption.rst - modules/end_to_end_encryption.rst
- modules/history_visibility.rst - modules/history_visibility.rst
- modules/push.rst - modules/push.rst
- modules/third_party_invites.rst
title_styles: ["=", "-", "~", "+", "^", "`"] title_styles: ["=", "-", "~", "+", "^", "`"]

@ -158,7 +158,7 @@ class MatrixUnits(Units):
"title": single_api.get("summary", ""), "title": single_api.get("summary", ""),
"desc": single_api.get("description", single_api.get("summary", "")), "desc": single_api.get("description", single_api.get("summary", "")),
"method": method.upper(), "method": method.upper(),
"path": full_path, "path": full_path.strip(),
"requires_auth": "security" in single_api, "requires_auth": "security" in single_api,
"rate_limited": 429 in single_api.get("responses", {}), "rate_limited": 429 in single_api.get("responses", {}),
"req_params": [], "req_params": [],

Loading…
Cancel
Save