Skip to content
This repository has been archived by the owner on Apr 26, 2024. It is now read-only.

Add JWT documentation and improve sample config #7776

Merged
merged 8 commits into from
Jul 6, 2020
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions docs/dev/jwt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# 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.)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should probably update the code to be the same as this, but I wanted to keep this PR to documentation only.


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.

## 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.
15 changes: 11 additions & 4 deletions docs/sample_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1804,12 +1804,19 @@ sso:
#template_dir: "res/templates"


# The JWT needs to contain a globally unique "sub" (subject) claim.
# Configure the non-standard JSON Web Token login type.
#
# Each JSON Web Token needs to contain a "sub" (subject) claim, which is
# used as the localpart of the mxid.
#
#jwt_config:
# enabled: true
# secret: "a secret"
# algorithm: "HS256"
# Uncomment the following to enable JWT logins.
#enabled: true
# This is either the private secret or the public key used to decode
# the contents of the JWT.
#secret: "a secret"
# The algorithm used to sign the JWT.
#algorithm: "HS256"


password_config:
Expand Down
15 changes: 11 additions & 4 deletions synapse/config/jwt_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,17 @@ def read_config(self, config, **kwargs):

def generate_config_section(self, **kwargs):
return """\
# The JWT needs to contain a globally unique "sub" (subject) claim.
# Configure the non-standard JSON Web Token login type.
#
# Each JSON Web Token needs to contain a "sub" (subject) claim, which is
# used as the localpart of the mxid.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no guarantee the sub will meet mxid character restrictions. How do we map under this scenario?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently it is assumed that it meets the restrictions.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I looked at this again since I wasn't 100% sure this was true and the string just gets jammed into one of our UserID objects:

user = payload.get("sub", None)
if user is None:
raise LoginError(401, "Invalid JWT", errcode=Codes.UNAUTHORIZED)
user_id = UserID(user, self.hs.hostname).to_string()

So yeah, this is essentially completely unchecked. 😢

#
#jwt_config:
# enabled: true
# secret: "a secret"
# algorithm: "HS256"
# Uncomment the following to enable JWT logins.
#enabled: true
# This is either the private secret or the public key used to decode
# the contents of the JWT.
#secret: "a secret"
# The algorithm used to sign the JWT.
#algorithm: "HS256"
"""
44 changes: 25 additions & 19 deletions synapse/rest/client/v1/login.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -26,7 +27,7 @@
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

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -113,7 +114,7 @@ def __init__(self, hs):
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})
Expand Down Expand Up @@ -141,10 +142,10 @@ def on_GET(self, request):

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)
Expand All @@ -153,9 +154,9 @@ async def on_POST(self, request):
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:
Expand All @@ -166,7 +167,7 @@ async def on_POST(self, request):
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):
"""Handle non-token/saml/jwt logins

Args:
Expand Down Expand Up @@ -288,25 +289,30 @@ async def _do_other_login(self, login_submission):
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,
):
"""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
Expand Down Expand Up @@ -340,7 +346,7 @@ async def _complete_login(

return result

async def do_token_login(self, login_submission):
async def _do_token_login(self, login_submission: JsonDict):
token = login_submission["token"]
auth_handler = self.auth_handler
user_id = await auth_handler.validate_short_term_login_token_and_get_user_id(
Expand All @@ -350,7 +356,7 @@ async def do_token_login(self, login_submission):
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):
token = login_submission.get("token", None)
if token is None:
raise LoginError(
Expand Down