1
0

Support nonces

This commit is contained in:
Erik Johnston
2015-09-26 17:38:40 +01:00
parent d01ef0c848
commit 1ca673a876
3 changed files with 75 additions and 56 deletions
+56 -7
View File
@@ -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
+4 -46
View File
@@ -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):
+15 -3
View File
@@ -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,