Support nonces
This commit is contained in:
@@ -18,7 +18,7 @@ from twisted.internet import defer
|
||||
from ._base import BaseHandler
|
||||
from synapse.api.constants import LoginType
|
||||
from synapse.types import UserID
|
||||
from synapse.api.errors import LoginError, Codes
|
||||
from synapse.api.errors import SynapseError, LoginError, Codes
|
||||
from synapse.util.async import run_on_reactor
|
||||
|
||||
from twisted.web.client import PartialDownloadError
|
||||
@@ -33,6 +33,8 @@ import synapse.util.stringutils as stringutils
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
MACAROON_TYPE_LOGIN_TOKEN = "st_login"
|
||||
|
||||
|
||||
class AuthHandler(BaseHandler):
|
||||
|
||||
@@ -46,6 +48,22 @@ class AuthHandler(BaseHandler):
|
||||
}
|
||||
self.sessions = {}
|
||||
|
||||
self._nonces = {}
|
||||
|
||||
self.clock.looping_call(self._prune_nonce, 60 * 1000)
|
||||
|
||||
def _prune_nonce(self):
|
||||
now = self.clock.time_msec()
|
||||
self._nonces = {
|
||||
user_id: {
|
||||
nonce: nonce_dict
|
||||
for nonce, nonce_dict in user_dict.items()
|
||||
if nonce_dict.get("expiry", 0) < now - 60 * 1000
|
||||
}
|
||||
for user_id, user_dict in self._nonces.items()
|
||||
if user_dict
|
||||
}
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def check_auth(self, flows, clientdict, clientip):
|
||||
"""
|
||||
@@ -301,15 +319,16 @@ class AuthHandler(BaseHandler):
|
||||
defer.returnValue((user_id, access_token, refresh_token))
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def do_short_term_token_login(self, token, user_id):
|
||||
def do_short_term_token_login(self, token, user_id, client_nonce):
|
||||
macaroon_exact_caveats = [
|
||||
"gen = 1",
|
||||
"type = st_login",
|
||||
"type = %s" % (MACAROON_TYPE_LOGIN_TOKEN,),
|
||||
"user_id = %s" % (user_id,)
|
||||
]
|
||||
|
||||
macaroon_general_caveats = [
|
||||
self._verify_macaroon_expiry
|
||||
self._verify_macaroon_expiry,
|
||||
lambda c: self._verify_nonce(c, user_id, client_nonce)
|
||||
]
|
||||
|
||||
try:
|
||||
@@ -338,7 +357,8 @@ class AuthHandler(BaseHandler):
|
||||
}
|
||||
|
||||
defer.returnValue(result)
|
||||
except (pymacaroons.exceptions.MacaroonException, TypeError, ValueError):
|
||||
except (pymacaroons.exceptions.MacaroonException, TypeError, ValueError) as e:
|
||||
logger.info("Invalid token: %s", e.message)
|
||||
raise LoginError(403, "Invalid token", errcode=Codes.FORBIDDEN)
|
||||
|
||||
def _verify_macaroon_expiry(self, caveat):
|
||||
@@ -350,12 +370,41 @@ class AuthHandler(BaseHandler):
|
||||
now = self.hs.get_clock().time_msec()
|
||||
return now < expiry
|
||||
|
||||
def make_short_term_token(self, user_id):
|
||||
def _verify_nonce(self, caveat, user_id, client_nonce):
|
||||
prefix = "nonce = "
|
||||
if not caveat.startswith(prefix):
|
||||
return False
|
||||
|
||||
user_dict = self._nonces.get(user_id, {})
|
||||
|
||||
nonce = caveat[len(prefix):]
|
||||
does_match = (
|
||||
nonce in user_dict
|
||||
and user_dict[nonce].get("client_nonce", None) in (None, client_nonce)
|
||||
)
|
||||
|
||||
if does_match:
|
||||
user_dict[nonce] = client_nonce
|
||||
|
||||
return does_match
|
||||
|
||||
def make_short_term_token(self, user_id, nonce):
|
||||
user_nonces = self._nonces.setdefault(user_id, {})
|
||||
if user_nonces.get(nonce, {}).get("client_nonce", None) is not None:
|
||||
raise SynapseError(400, "nonce already used")
|
||||
|
||||
macaroon = self._generate_base_macaroon(user_id)
|
||||
macaroon.add_first_party_caveat("type = st_login")
|
||||
macaroon.add_first_party_caveat("type = %s" % (MACAROON_TYPE_LOGIN_TOKEN,))
|
||||
now = self.hs.get_clock().time_msec()
|
||||
expiry = now + (60 * 1000)
|
||||
macaroon.add_first_party_caveat("time < %d" % (expiry,))
|
||||
macaroon.add_first_party_caveat("nonce = %s" % (nonce,))
|
||||
|
||||
user_nonces[nonce] = {
|
||||
"client_nonce": None,
|
||||
"expiry": expiry,
|
||||
}
|
||||
|
||||
return macaroon.serialize()
|
||||
|
||||
@defer.inlineCallbacks
|
||||
|
||||
@@ -77,7 +77,10 @@ class LoginRestServlet(ClientV1RestServlet):
|
||||
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)
|
||||
client_nonce = login_submission["nonce"]
|
||||
result = yield auth_handler.do_short_term_token_login(
|
||||
token, user_id, client_nonce
|
||||
)
|
||||
defer.returnValue((200, result))
|
||||
else:
|
||||
raise SynapseError(400, "Bad login type.")
|
||||
@@ -112,51 +115,6 @@ 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):
|
||||
|
||||
@@ -17,6 +17,7 @@ from twisted.web.server import NOT_DONE_YET
|
||||
from twisted.internet import defer, threads
|
||||
|
||||
from synapse.api.errors import CodeMessageException
|
||||
from synapse.util.stringutils import random_string
|
||||
|
||||
import simplejson
|
||||
import logging
|
||||
@@ -46,7 +47,16 @@ class LoginQRResource(Resource):
|
||||
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())
|
||||
|
||||
nonce = request.path.split("/")[-1]
|
||||
|
||||
if not nonce:
|
||||
nonce = random_string(10)
|
||||
|
||||
image = yield self.make_short_term_qr_code(
|
||||
auth_user.to_string(), nonce
|
||||
)
|
||||
|
||||
request.setHeader(b"Content-Type", b"image/png")
|
||||
|
||||
image.save(request)
|
||||
@@ -54,16 +64,18 @@ class LoginQRResource(Resource):
|
||||
except CodeMessageException as e:
|
||||
logger.info("Returning: %s", e)
|
||||
request.setResponseCode(e.code)
|
||||
request.write("%s: %s" % (e.code, e.message))
|
||||
request.finish()
|
||||
except Exception:
|
||||
logger.exception("Exception while generating token")
|
||||
request.setResponseCode(500)
|
||||
request.write("Internal server error")
|
||||
request.finish()
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def make_short_term_qr_code(self, user_id):
|
||||
def make_short_term_qr_code(self, user_id, nonce):
|
||||
h = self.handlers.auth_handler
|
||||
token = h.make_short_term_token(user_id)
|
||||
token = h.make_short_term_token(user_id, nonce)
|
||||
|
||||
x509_certificate_bytes = crypto.dump_certificate(
|
||||
crypto.FILETYPE_ASN1,
|
||||
|
||||
Reference in New Issue
Block a user