forked from MirrorHub/synapse
Add documentation for JWT login type and improve sample config. (#7776)
This commit is contained in:
parent
6d687ebba1
commit
2a266f4511
5 changed files with 180 additions and 29 deletions
1
changelog.d/7776.doc
Normal file
1
changelog.d/7776.doc
Normal file
|
@ -0,0 +1 @@
|
|||
Improve the documentation of the non-standard JSON web token login type.
|
90
docs/jwt.md
Normal file
90
docs/jwt.md
Normal file
|
@ -0,0 +1,90 @@
|
|||
# JWT Login Type
|
||||
|
||||
Synapse comes with a non-standard login type to support
|
||||
[JSON Web Tokens](https://en.wikipedia.org/wiki/JSON_Web_Token). In general the
|
||||
documentation for
|
||||
[the login endpoint](https://matrix.org/docs/spec/client_server/r0.6.1#login)
|
||||
is still valid (and the mechanism works similarly to the
|
||||
[token based login](https://matrix.org/docs/spec/client_server/r0.6.1#token-based)).
|
||||
|
||||
To log in using a JSON Web Token, clients should submit a `/login` request as
|
||||
follows:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "org.matrix.login.jwt",
|
||||
"token": "<jwt>"
|
||||
}
|
||||
```
|
||||
|
||||
Note that the login type of `m.login.jwt` is supported, but is deprecated. This
|
||||
will be removed in a future version of Synapse.
|
||||
|
||||
The `jwt` should encode the local part of the user ID as the standard `sub`
|
||||
claim. In the case that the token is not valid, the homeserver must respond with
|
||||
`401 Unauthorized` and an error code of `M_UNAUTHORIZED`.
|
||||
|
||||
(Note that this differs from the token based logins which return a
|
||||
`403 Forbidden` and an error code of `M_FORBIDDEN` if an error occurs.)
|
||||
|
||||
As with other login types, there are additional fields (e.g. `device_id` and
|
||||
`initial_device_display_name`) which can be included in the above request.
|
||||
|
||||
## Preparing Synapse
|
||||
|
||||
The JSON Web Token integration in Synapse uses the
|
||||
[`PyJWT`](https://pypi.org/project/pyjwt/) library, which must be installed
|
||||
as follows:
|
||||
|
||||
* The relevant libraries are included in the Docker images and Debian packages
|
||||
provided by `matrix.org` so no further action is needed.
|
||||
|
||||
* If you installed Synapse into a virtualenv, run `/path/to/env/bin/pip
|
||||
install synapse[pyjwt]` to install the necessary dependencies.
|
||||
|
||||
* For other installation mechanisms, see the documentation provided by the
|
||||
maintainer.
|
||||
|
||||
To enable the JSON web token integration, you should then add an `jwt_config` section
|
||||
to your configuration file (or uncomment the `enabled: true` line in the
|
||||
existing section). See [sample_config.yaml](./sample_config.yaml) for some
|
||||
sample settings.
|
||||
|
||||
## How to test JWT as a developer
|
||||
|
||||
Although JSON Web Tokens are typically generated from an external server, the
|
||||
examples below use [PyJWT](https://pyjwt.readthedocs.io/en/latest/) directly.
|
||||
|
||||
1. Configure Synapse with JWT logins:
|
||||
|
||||
```yaml
|
||||
jwt_config:
|
||||
enabled: true
|
||||
secret: "my-secret-token"
|
||||
algorithm: "HS256"
|
||||
```
|
||||
2. Generate a JSON web token:
|
||||
|
||||
```bash
|
||||
$ pyjwt --key=my-secret-token --alg=HS256 encode sub=test-user
|
||||
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ0ZXN0LXVzZXIifQ.Ag71GT8v01UO3w80aqRPTeuVPBIBZkYhNTJJ-_-zQIc
|
||||
```
|
||||
3. Query for the login types and ensure `org.matrix.login.jwt` is there:
|
||||
|
||||
```bash
|
||||
curl http://localhost:8080/_matrix/client/r0/login
|
||||
```
|
||||
4. Login used the generated JSON web token from above:
|
||||
|
||||
```bash
|
||||
$ curl http://localhost:8082/_matrix/client/r0/login -X POST \
|
||||
--data '{"type":"org.matrix.login.jwt","token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ0ZXN0LXVzZXIifQ.Ag71GT8v01UO3w80aqRPTeuVPBIBZkYhNTJJ-_-zQIc"}'
|
||||
{
|
||||
"access_token": "<access token>",
|
||||
"device_id": "ACBDEFGHI",
|
||||
"home_server": "localhost:8080",
|
||||
"user_id": "@test-user:localhost:8480"
|
||||
}
|
||||
```
|
||||
|
||||
You should now be able to use the returned access token to query the client API.
|
|
@ -1804,12 +1804,39 @@ sso:
|
|||
#template_dir: "res/templates"
|
||||
|
||||
|
||||
# The JWT needs to contain a globally unique "sub" (subject) claim.
|
||||
# JSON web token integration. The following settings can be used to make
|
||||
# Synapse JSON web tokens for authentication, instead of its internal
|
||||
# password database.
|
||||
#
|
||||
# Each JSON Web Token needs to contain a "sub" (subject) claim, which is
|
||||
# used as the localpart of the mxid.
|
||||
#
|
||||
# Note that this is a non-standard login type and client support is
|
||||
# expected to be non-existant.
|
||||
#
|
||||
# See https://github.com/matrix-org/synapse/blob/master/docs/jwt.md.
|
||||
#
|
||||
#jwt_config:
|
||||
# enabled: true
|
||||
# secret: "a secret"
|
||||
# algorithm: "HS256"
|
||||
# Uncomment the following to enable authorization using JSON web
|
||||
# tokens. Defaults to false.
|
||||
#
|
||||
#enabled: true
|
||||
|
||||
# This is either the private shared secret or the public key used to
|
||||
# decode the contents of the JSON web token.
|
||||
#
|
||||
# Required if 'enabled' is true.
|
||||
#
|
||||
#secret: "provided-by-your-issuer"
|
||||
|
||||
# The algorithm used to sign the JSON web token.
|
||||
#
|
||||
# Supported algorithms are listed at
|
||||
# https://pyjwt.readthedocs.io/en/latest/algorithms.html
|
||||
#
|
||||
# Required if 'enabled' is true.
|
||||
#
|
||||
#algorithm: "provided-by-your-issuer"
|
||||
|
||||
|
||||
password_config:
|
||||
|
|
|
@ -45,10 +45,37 @@ class JWTConfig(Config):
|
|||
|
||||
def generate_config_section(self, **kwargs):
|
||||
return """\
|
||||
# The JWT needs to contain a globally unique "sub" (subject) claim.
|
||||
# JSON web token integration. The following settings can be used to make
|
||||
# Synapse JSON web tokens for authentication, instead of its internal
|
||||
# password database.
|
||||
#
|
||||
# Each JSON Web Token needs to contain a "sub" (subject) claim, which is
|
||||
# used as the localpart of the mxid.
|
||||
#
|
||||
# Note that this is a non-standard login type and client support is
|
||||
# expected to be non-existant.
|
||||
#
|
||||
# See https://github.com/matrix-org/synapse/blob/master/docs/jwt.md.
|
||||
#
|
||||
#jwt_config:
|
||||
# enabled: true
|
||||
# secret: "a secret"
|
||||
# algorithm: "HS256"
|
||||
# Uncomment the following to enable authorization using JSON web
|
||||
# tokens. Defaults to false.
|
||||
#
|
||||
#enabled: true
|
||||
|
||||
# This is either the private shared secret or the public key used to
|
||||
# decode the contents of the JSON web token.
|
||||
#
|
||||
# Required if 'enabled' is true.
|
||||
#
|
||||
#secret: "provided-by-your-issuer"
|
||||
|
||||
# The algorithm used to sign the JSON web token.
|
||||
#
|
||||
# Supported algorithms are listed at
|
||||
# https://pyjwt.readthedocs.io/en/latest/algorithms.html
|
||||
#
|
||||
# Required if 'enabled' is true.
|
||||
#
|
||||
#algorithm: "provided-by-your-issuer"
|
||||
"""
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
# limitations under the License.
|
||||
|
||||
import logging
|
||||
from typing import Awaitable, Callable, Dict, Optional
|
||||
|
||||
from synapse.api.errors import Codes, LoginError, SynapseError
|
||||
from synapse.api.ratelimiting import Ratelimiter
|
||||
|
@ -26,7 +27,7 @@ from synapse.http.servlet import (
|
|||
from synapse.http.site import SynapseRequest
|
||||
from synapse.rest.client.v2_alpha._base import client_patterns
|
||||
from synapse.rest.well_known import WellKnownBuilder
|
||||
from synapse.types import UserID
|
||||
from synapse.types import JsonDict, UserID
|
||||
from synapse.util.msisdn import phone_number_to_msisdn
|
||||
from synapse.util.threepids import canonicalise_email
|
||||
|
||||
|
@ -114,7 +115,7 @@ class LoginRestServlet(RestServlet):
|
|||
burst_count=self.hs.config.rc_login_failed_attempts.burst_count,
|
||||
)
|
||||
|
||||
def on_GET(self, request):
|
||||
def on_GET(self, request: SynapseRequest):
|
||||
flows = []
|
||||
if self.jwt_enabled:
|
||||
flows.append({"type": LoginRestServlet.JWT_TYPE})
|
||||
|
@ -142,10 +143,10 @@ class LoginRestServlet(RestServlet):
|
|||
|
||||
return 200, {"flows": flows}
|
||||
|
||||
def on_OPTIONS(self, request):
|
||||
def on_OPTIONS(self, request: SynapseRequest):
|
||||
return 200, {}
|
||||
|
||||
async def on_POST(self, request):
|
||||
async def on_POST(self, request: SynapseRequest):
|
||||
self._address_ratelimiter.ratelimit(request.getClientIP())
|
||||
|
||||
login_submission = parse_json_object_from_request(request)
|
||||
|
@ -154,9 +155,9 @@ class LoginRestServlet(RestServlet):
|
|||
login_submission["type"] == LoginRestServlet.JWT_TYPE
|
||||
or login_submission["type"] == LoginRestServlet.JWT_TYPE_DEPRECATED
|
||||
):
|
||||
result = await self.do_jwt_login(login_submission)
|
||||
result = await self._do_jwt_login(login_submission)
|
||||
elif login_submission["type"] == LoginRestServlet.TOKEN_TYPE:
|
||||
result = await self.do_token_login(login_submission)
|
||||
result = await self._do_token_login(login_submission)
|
||||
else:
|
||||
result = await self._do_other_login(login_submission)
|
||||
except KeyError:
|
||||
|
@ -167,14 +168,14 @@ class LoginRestServlet(RestServlet):
|
|||
result["well_known"] = well_known_data
|
||||
return 200, result
|
||||
|
||||
async def _do_other_login(self, login_submission):
|
||||
async def _do_other_login(self, login_submission: JsonDict) -> Dict[str, str]:
|
||||
"""Handle non-token/saml/jwt logins
|
||||
|
||||
Args:
|
||||
login_submission:
|
||||
|
||||
Returns:
|
||||
dict: HTTP response
|
||||
HTTP response
|
||||
"""
|
||||
# Log the request we got, but only certain fields to minimise the chance of
|
||||
# logging someone's password (even if they accidentally put it in the wrong
|
||||
|
@ -292,25 +293,30 @@ class LoginRestServlet(RestServlet):
|
|||
return result
|
||||
|
||||
async def _complete_login(
|
||||
self, user_id, login_submission, callback=None, create_non_existent_users=False
|
||||
):
|
||||
self,
|
||||
user_id: str,
|
||||
login_submission: JsonDict,
|
||||
callback: Optional[
|
||||
Callable[[Dict[str, str]], Awaitable[Dict[str, str]]]
|
||||
] = None,
|
||||
create_non_existent_users: bool = False,
|
||||
) -> Dict[str, str]:
|
||||
"""Called when we've successfully authed the user and now need to
|
||||
actually login them in (e.g. create devices). This gets called on
|
||||
all succesful logins.
|
||||
all successful logins.
|
||||
|
||||
Applies the ratelimiting for succesful login attempts against an
|
||||
Applies the ratelimiting for successful login attempts against an
|
||||
account.
|
||||
|
||||
Args:
|
||||
user_id (str): ID of the user to register.
|
||||
login_submission (dict): Dictionary of login information.
|
||||
callback (func|None): Callback function to run after registration.
|
||||
create_non_existent_users (bool): Whether to create the user if
|
||||
they don't exist. Defaults to False.
|
||||
user_id: ID of the user to register.
|
||||
login_submission: Dictionary of login information.
|
||||
callback: Callback function to run after registration.
|
||||
create_non_existent_users: Whether to create the user if they don't
|
||||
exist. Defaults to False.
|
||||
|
||||
Returns:
|
||||
result (Dict[str,str]): Dictionary of account information after
|
||||
successful registration.
|
||||
result: Dictionary of account information after successful registration.
|
||||
"""
|
||||
|
||||
# Before we actually log them in we check if they've already logged in
|
||||
|
@ -344,7 +350,7 @@ class LoginRestServlet(RestServlet):
|
|||
|
||||
return result
|
||||
|
||||
async def do_token_login(self, login_submission):
|
||||
async def _do_token_login(self, login_submission: JsonDict) -> Dict[str, str]:
|
||||
token = login_submission["token"]
|
||||
auth_handler = self.auth_handler
|
||||
user_id = await auth_handler.validate_short_term_login_token_and_get_user_id(
|
||||
|
@ -354,7 +360,7 @@ class LoginRestServlet(RestServlet):
|
|||
result = await self._complete_login(user_id, login_submission)
|
||||
return result
|
||||
|
||||
async def do_jwt_login(self, login_submission):
|
||||
async def _do_jwt_login(self, login_submission: JsonDict) -> Dict[str, str]:
|
||||
token = login_submission.get("token", None)
|
||||
if token is None:
|
||||
raise LoginError(
|
||||
|
|
Loading…
Reference in a new issue