mirror of
https://github.com/element-hq/synapse
synced 2024-10-03 02:42:40 +00:00
Add support for logging in via token. Also add QR code to server up token.
This commit is contained in:
parent
17795161c3
commit
936cdac6aa
5 changed files with 235 additions and 1 deletions
|
@ -117,6 +117,11 @@ class ServerConfig(Config):
|
|||
|
||||
self.content_addr = content_addr
|
||||
|
||||
client_addr = config.get("client_addr")
|
||||
if not client_addr:
|
||||
client_addr = content_addr
|
||||
self.client_addr = client_addr
|
||||
|
||||
def default_config(self, server_name, **kwargs):
|
||||
if ":" in server_name:
|
||||
bind_port = int(server_name.split(":")[1])
|
||||
|
@ -140,6 +145,9 @@ class ServerConfig(Config):
|
|||
# Whether to serve a web client from the HTTP/HTTPS root resource.
|
||||
web_client: True
|
||||
|
||||
# URL clients can use to talk to the server.
|
||||
client_addr: "https://%(server_name)s:%(bind_port)s"
|
||||
|
||||
# Set the soft limit on the number of file descriptors synapse can use
|
||||
# Zero is used to indicate synapse should set the soft limit to the
|
||||
# hard limit.
|
||||
|
|
|
@ -290,11 +290,74 @@ class AuthHandler(BaseHandler):
|
|||
user_id, password_hash = yield self._find_user_id_and_pwd_hash(user_id)
|
||||
self._check_password(user_id, password, password_hash)
|
||||
|
||||
res = yield self._issue_tokens(user_id)
|
||||
defer.returnValue(res)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def _issue_tokens(self, user_id):
|
||||
logger.info("Logging in user %s", user_id)
|
||||
access_token = yield self.issue_access_token(user_id)
|
||||
refresh_token = yield self.issue_refresh_token(user_id)
|
||||
defer.returnValue((user_id, access_token, refresh_token))
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def do_short_term_token_login(self, token, user_id):
|
||||
macaroon_exact_caveats = [
|
||||
"gen = 1",
|
||||
"type = st_login",
|
||||
"user_id = %s" % (user_id,)
|
||||
]
|
||||
|
||||
macaroon_general_caveats = [
|
||||
self._verify_macaroon_expiry
|
||||
]
|
||||
|
||||
try:
|
||||
macaroon = pymacaroons.Macaroon.deserialize(token)
|
||||
|
||||
v = pymacaroons.Verifier()
|
||||
for exact_caveat in macaroon_exact_caveats:
|
||||
v.satisfy_exact(exact_caveat)
|
||||
|
||||
for general_caveat in macaroon_general_caveats:
|
||||
v.satisfy_general(general_caveat)
|
||||
|
||||
verified = v.verify(macaroon, self.hs.config.macaroon_secret_key)
|
||||
if not verified:
|
||||
raise LoginError(403, "Invalid token", errcode=Codes.FORBIDDEN)
|
||||
|
||||
user_id, access_token, refresh_token = yield self._issue_tokens(
|
||||
user_id=user_id,
|
||||
)
|
||||
|
||||
result = {
|
||||
"user_id": user_id, # may have changed
|
||||
"access_token": access_token,
|
||||
"refresh_token": refresh_token,
|
||||
"home_server": self.hs.hostname,
|
||||
}
|
||||
|
||||
defer.returnValue(result)
|
||||
except (pymacaroons.exceptions.MacaroonException, TypeError, ValueError):
|
||||
raise LoginError(403, "Invalid token", errcode=Codes.FORBIDDEN)
|
||||
|
||||
def _verify_macaroon_expiry(self, caveat):
|
||||
prefix = "time < "
|
||||
if not caveat.startswith(prefix):
|
||||
return False
|
||||
|
||||
expiry = int(caveat[len(prefix):])
|
||||
now = self.hs.get_clock().time_msec()
|
||||
return now < expiry
|
||||
|
||||
def make_short_term_token(self, user_id):
|
||||
macaroon = self._generate_base_macaroon(user_id)
|
||||
macaroon.add_first_party_caveat("type = st_login")
|
||||
now = self.hs.get_clock().time_msec()
|
||||
expiry = now + (60 * 1000)
|
||||
macaroon.add_first_party_caveat("time < %d" % (expiry,))
|
||||
return macaroon.serialize()
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def _find_user_id_and_pwd_hash(self, user_id):
|
||||
"""Checks to see if a user with the given id exists. Will check case
|
||||
|
|
|
@ -27,6 +27,8 @@ from saml2 import BINDING_HTTP_POST
|
|||
from saml2 import config
|
||||
from saml2.client import Saml2Client
|
||||
|
||||
import pymacaroons
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -35,6 +37,7 @@ class LoginRestServlet(ClientV1RestServlet):
|
|||
PATTERN = client_path_pattern("/login$")
|
||||
PASS_TYPE = "m.login.password"
|
||||
SAML2_TYPE = "m.login.saml2"
|
||||
TOKEN_TYPE = "m.login.token"
|
||||
|
||||
def __init__(self, hs):
|
||||
super(LoginRestServlet, self).__init__(hs)
|
||||
|
@ -42,7 +45,10 @@ class LoginRestServlet(ClientV1RestServlet):
|
|||
self.saml2_enabled = hs.config.saml2_enabled
|
||||
|
||||
def on_GET(self, request):
|
||||
flows = [{"type": LoginRestServlet.PASS_TYPE}]
|
||||
flows = [
|
||||
{"type": LoginRestServlet.PASS_TYPE},
|
||||
{"type": LoginRestServlet.TOKEN_TYPE}
|
||||
]
|
||||
if self.saml2_enabled:
|
||||
flows.append({"type": LoginRestServlet.SAML2_TYPE})
|
||||
return (200, {"flows": flows})
|
||||
|
@ -67,6 +73,12 @@ class LoginRestServlet(ClientV1RestServlet):
|
|||
"uri": "%s%s" % (self.idp_redirect_url, relay_state)
|
||||
}
|
||||
defer.returnValue((200, result))
|
||||
elif login_submission["type"] == LoginRestServlet.TOKEN_TYPE:
|
||||
auth_handler = self.handlers.auth_handler
|
||||
token = login_submission["token"]
|
||||
user_id = login_submission["user"]
|
||||
result = yield auth_handler.do_short_term_token_login(token, user_id)
|
||||
defer.returnValue(result)
|
||||
else:
|
||||
raise SynapseError(400, "Bad login type.")
|
||||
except KeyError:
|
||||
|
@ -100,6 +112,60 @@ class LoginRestServlet(ClientV1RestServlet):
|
|||
|
||||
defer.returnValue((200, result))
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def do_short_term_token_login(self, login_submission):
|
||||
token = login_submission["token"]
|
||||
user_id = login_submission["user"]
|
||||
|
||||
macaroon_exact_caveats = [
|
||||
"gen = 1",
|
||||
"type = st_login",
|
||||
"user_id = %s" % (user_id,)
|
||||
]
|
||||
|
||||
macaroon_general_caveats = [
|
||||
self._verify_macaroon_expiry
|
||||
]
|
||||
|
||||
try:
|
||||
macaroon = pymacaroons.Macaroon.deserialize(token)
|
||||
|
||||
v = pymacaroons.Verifier()
|
||||
for exact_caveat in macaroon_exact_caveats:
|
||||
v.satisfy_exact(exact_caveat)
|
||||
|
||||
for general_caveat in macaroon_general_caveats:
|
||||
v.satisfy_general(general_caveat)
|
||||
|
||||
verified = v.verify(macaroon, self.hs.config.macaroon_secret_key)
|
||||
if not verified:
|
||||
raise SynapseError(400, "Invalid token.")
|
||||
|
||||
auth_handler = self.handlers.auth_handler
|
||||
user_id, access_token, refresh_token = yield auth_handler.issue_tokens(
|
||||
user_id=user_id,
|
||||
)
|
||||
|
||||
result = {
|
||||
"user_id": user_id, # may have changed
|
||||
"access_token": access_token,
|
||||
"refresh_token": refresh_token,
|
||||
"home_server": self.hs.hostname,
|
||||
}
|
||||
|
||||
defer.returnValue(result)
|
||||
except (pymacaroons.exceptions.MacaroonException, TypeError, ValueError):
|
||||
raise SynapseError(400, "Invalid token.")
|
||||
|
||||
def _verify_macaroon_expiry(self, caveat):
|
||||
prefix = "time < "
|
||||
if not caveat.startswith(prefix):
|
||||
return False
|
||||
|
||||
expiry = int(caveat[len(prefix):])
|
||||
now = self.hs.get_clock().time_msec()
|
||||
return now < expiry
|
||||
|
||||
|
||||
class LoginFallbackRestServlet(ClientV1RestServlet):
|
||||
PATTERN = client_path_pattern("/login/fallback$")
|
||||
|
|
95
synapse/rest/media/v1/login_qr_resource.py
Normal file
95
synapse/rest/media/v1/login_qr_resource.py
Normal file
|
@ -0,0 +1,95 @@
|
|||
# Copyright 2015 OpenMarket Ltd
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from twisted.web.resource import Resource
|
||||
from twisted.web.server import NOT_DONE_YET
|
||||
from twisted.internet import defer, threads
|
||||
|
||||
from synapse.api.errors import CodeMessageException
|
||||
|
||||
import simplejson
|
||||
import logging
|
||||
|
||||
from unpaddedbase64 import encode_base64
|
||||
from hashlib import sha256
|
||||
from OpenSSL import crypto
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class LoginQRResource(Resource):
|
||||
isLeaf = True
|
||||
|
||||
def __init__(self, hs):
|
||||
Resource.__init__(self)
|
||||
self.hs = hs
|
||||
self.auth = hs.get_auth()
|
||||
self.handlers = hs.get_handlers()
|
||||
self.config = hs.get_config()
|
||||
|
||||
def render_GET(self, request):
|
||||
self._async_render_GET(request)
|
||||
return NOT_DONE_YET
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def _async_render_GET(self, request):
|
||||
try:
|
||||
auth_user, _ = yield self.auth.get_user_by_req(request)
|
||||
image = yield self.make_short_term_qr_code(auth_user.to_string())
|
||||
request.setHeader(b"Content-Type", b"image/png")
|
||||
|
||||
image.save(request)
|
||||
request.finish()
|
||||
except CodeMessageException as e:
|
||||
logger.info("Returning: %s", e)
|
||||
request.setResponseCode(e.code)
|
||||
request.finish()
|
||||
except Exception:
|
||||
logger.exception("Exception while generating token")
|
||||
request.setResponseCode(500)
|
||||
request.finish()
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def make_short_term_qr_code(self, user_id):
|
||||
h = self.handlers.auth_handler
|
||||
token = h.make_short_term_token(user_id)
|
||||
|
||||
x509_certificate_bytes = crypto.dump_certificate(
|
||||
crypto.FILETYPE_ASN1,
|
||||
self.config.tls_certificate
|
||||
)
|
||||
|
||||
sha256_fingerprint = sha256(x509_certificate_bytes).digest()
|
||||
|
||||
def gen():
|
||||
import qrcode
|
||||
qr = qrcode.QRCode(
|
||||
version=1,
|
||||
error_correction=qrcode.constants.ERROR_CORRECT_L,
|
||||
box_size=5,
|
||||
)
|
||||
qr.add_data(simplejson.dumps({
|
||||
"user_id": user_id,
|
||||
"token": token,
|
||||
"homeserver_url": self.config.client_addr,
|
||||
"fingerprints": [{
|
||||
"hash_type": "SHA256",
|
||||
"bytes": encode_base64(sha256_fingerprint),
|
||||
}],
|
||||
}))
|
||||
qr.make(fit=True)
|
||||
return qr.make_image()
|
||||
|
||||
res = yield threads.deferToThread(gen)
|
||||
defer.returnValue(res)
|
|
@ -17,6 +17,7 @@ from .upload_resource import UploadResource
|
|||
from .download_resource import DownloadResource
|
||||
from .thumbnail_resource import ThumbnailResource
|
||||
from .identicon_resource import IdenticonResource
|
||||
from .login_qr_resource import LoginQRResource
|
||||
from .filepath import MediaFilePaths
|
||||
|
||||
from twisted.web.resource import Resource
|
||||
|
@ -78,3 +79,4 @@ class MediaRepositoryResource(Resource):
|
|||
self.putChild("download", DownloadResource(hs, filepaths))
|
||||
self.putChild("thumbnail", ThumbnailResource(hs, filepaths))
|
||||
self.putChild("identicon", IdenticonResource())
|
||||
self.putChild("login_qr", LoginQRResource(hs))
|
||||
|
|
Loading…
Reference in a new issue