Add documentation for JWT login type and improve sample config. (#7776)

This commit is contained in:
Patrick Cloke 2020-07-06 08:31:51 -04:00 committed by GitHub
parent 6d687ebba1
commit 2a266f4511
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 180 additions and 29 deletions

1
changelog.d/7776.doc Normal file
View file

@ -0,0 +1 @@
Improve the documentation of the non-standard JSON web token login type.

90
docs/jwt.md Normal file
View 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.

View file

@ -1804,12 +1804,39 @@ sso:
#template_dir: "res/templates" #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: #jwt_config:
# enabled: true # Uncomment the following to enable authorization using JSON web
# secret: "a secret" # tokens. Defaults to false.
# algorithm: "HS256" #
#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: password_config:

View file

@ -45,10 +45,37 @@ class JWTConfig(Config):
def generate_config_section(self, **kwargs): def generate_config_section(self, **kwargs):
return """\ 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: #jwt_config:
# enabled: true # Uncomment the following to enable authorization using JSON web
# secret: "a secret" # tokens. Defaults to false.
# algorithm: "HS256" #
#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"
""" """

View file

@ -14,6 +14,7 @@
# limitations under the License. # limitations under the License.
import logging import logging
from typing import Awaitable, Callable, Dict, Optional
from synapse.api.errors import Codes, LoginError, SynapseError from synapse.api.errors import Codes, LoginError, SynapseError
from synapse.api.ratelimiting import Ratelimiter from synapse.api.ratelimiting import Ratelimiter
@ -26,7 +27,7 @@ from synapse.http.servlet import (
from synapse.http.site import SynapseRequest from synapse.http.site import SynapseRequest
from synapse.rest.client.v2_alpha._base import client_patterns from synapse.rest.client.v2_alpha._base import client_patterns
from synapse.rest.well_known import WellKnownBuilder 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.msisdn import phone_number_to_msisdn
from synapse.util.threepids import canonicalise_email 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, burst_count=self.hs.config.rc_login_failed_attempts.burst_count,
) )
def on_GET(self, request): def on_GET(self, request: SynapseRequest):
flows = [] flows = []
if self.jwt_enabled: if self.jwt_enabled:
flows.append({"type": LoginRestServlet.JWT_TYPE}) flows.append({"type": LoginRestServlet.JWT_TYPE})
@ -142,10 +143,10 @@ class LoginRestServlet(RestServlet):
return 200, {"flows": flows} return 200, {"flows": flows}
def on_OPTIONS(self, request): def on_OPTIONS(self, request: SynapseRequest):
return 200, {} return 200, {}
async def on_POST(self, request): async def on_POST(self, request: SynapseRequest):
self._address_ratelimiter.ratelimit(request.getClientIP()) self._address_ratelimiter.ratelimit(request.getClientIP())
login_submission = parse_json_object_from_request(request) login_submission = parse_json_object_from_request(request)
@ -154,9 +155,9 @@ class LoginRestServlet(RestServlet):
login_submission["type"] == LoginRestServlet.JWT_TYPE login_submission["type"] == LoginRestServlet.JWT_TYPE
or login_submission["type"] == LoginRestServlet.JWT_TYPE_DEPRECATED 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: elif login_submission["type"] == LoginRestServlet.TOKEN_TYPE:
result = await self.do_token_login(login_submission) result = await self._do_token_login(login_submission)
else: else:
result = await self._do_other_login(login_submission) result = await self._do_other_login(login_submission)
except KeyError: except KeyError:
@ -167,14 +168,14 @@ class LoginRestServlet(RestServlet):
result["well_known"] = well_known_data result["well_known"] = well_known_data
return 200, result 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 """Handle non-token/saml/jwt logins
Args: Args:
login_submission: login_submission:
Returns: Returns:
dict: HTTP response HTTP response
""" """
# Log the request we got, but only certain fields to minimise the chance of # 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 # logging someone's password (even if they accidentally put it in the wrong
@ -292,25 +293,30 @@ class LoginRestServlet(RestServlet):
return result return result
async def _complete_login( 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 """Called when we've successfully authed the user and now need to
actually login them in (e.g. create devices). This gets called on 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. account.
Args: Args:
user_id (str): ID of the user to register. user_id: ID of the user to register.
login_submission (dict): Dictionary of login information. login_submission: Dictionary of login information.
callback (func|None): Callback function to run after registration. callback: Callback function to run after registration.
create_non_existent_users (bool): Whether to create the user if create_non_existent_users: Whether to create the user if they don't
they don't exist. Defaults to False. exist. Defaults to False.
Returns: Returns:
result (Dict[str,str]): Dictionary of account information after result: Dictionary of account information after successful registration.
successful registration.
""" """
# Before we actually log them in we check if they've already logged in # Before we actually log them in we check if they've already logged in
@ -344,7 +350,7 @@ class LoginRestServlet(RestServlet):
return result 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"] token = login_submission["token"]
auth_handler = self.auth_handler auth_handler = self.auth_handler
user_id = await auth_handler.validate_short_term_login_token_and_get_user_id( 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) result = await self._complete_login(user_id, login_submission)
return result 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) token = login_submission.get("token", None)
if token is None: if token is None:
raise LoginError( raise LoginError(