mirror of
https://github.com/element-hq/synapse.git
synced 2024-12-18 08:54:54 +03:00
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"
|
#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:
|
||||||
|
|
|
@ -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"
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -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(
|
||||||
|
|
Loading…
Reference in a new issue