summary refs log tree commit diff
path: root/synapse/handlers
diff options
context:
space:
mode:
authorSteven Hammerton <steven.hammerton@openmarket.com>2015-11-05 14:01:12 +0000
committerSteven Hammerton <steven.hammerton@openmarket.com>2015-11-05 14:06:48 +0000
commit414a4a71b4421a376feb4e3e4ec5ae997fa289b2 (patch)
treea5b22aa2c878b465d8c30a506701f7be0eec3045 /synapse/handlers
parentAdd service URL to CAS config (diff)
downloadsynapse-414a4a71b4421a376feb4e3e4ec5ae997fa289b2.tar.xz
Allow hs to do CAS login completely and issue the client with a login token that can be redeemed for the usual successful login response
Diffstat (limited to 'synapse/handlers')
-rw-r--r--synapse/handlers/auth.py76
1 files changed, 73 insertions, 3 deletions
diff --git a/synapse/handlers/auth.py b/synapse/handlers/auth.py
index 1b11dbdffd..7a85883aa6 100644
--- a/synapse/handlers/auth.py
+++ b/synapse/handlers/auth.py
@@ -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 AuthError, LoginError, Codes
 from synapse.util.async import run_on_reactor
 
 from twisted.web.client import PartialDownloadError
@@ -46,6 +46,13 @@ class AuthHandler(BaseHandler):
         }
         self.bcrypt_rounds = hs.config.bcrypt_rounds
         self.sessions = {}
+        self.INVALID_TOKEN_HTTP_STATUS = 401
+        self._KNOWN_CAVEAT_PREFIXES = set([
+            "gen = ",
+            "type = ",
+            "time < ",
+            "user_id = ",
+        ])
 
     @defer.inlineCallbacks
     def check_auth(self, flows, clientdict, clientip):
@@ -297,10 +304,11 @@ class AuthHandler(BaseHandler):
         defer.returnValue((user_id, access_token, refresh_token))
 
     @defer.inlineCallbacks
-    def login_with_cas_user_id(self, user_id):
+    def login_with_user_id(self, user_id):
         """
         Authenticates the user with the given user ID,
-        intended to have been captured from a CAS response
+        it is intended that the authentication of the user has
+        already been verified by other mechanism (e.g. CAS)
 
         Args:
             user_id (str): User ID
@@ -393,6 +401,17 @@ class AuthHandler(BaseHandler):
         ))
         return m.serialize()
 
+    def generate_short_term_login_token(self, user_id):
+        macaroon = self._generate_base_macaroon(user_id)
+        macaroon.add_first_party_caveat("type = login")
+        now = self.hs.get_clock().time_msec()
+        expiry = now + (2 * 60 * 1000)
+        macaroon.add_first_party_caveat("time < %d" % (expiry,))
+        return macaroon.serialize()
+
+    def validate_short_term_login_token_and_get_user_id(self, login_token):
+        return self._validate_macaroon_and_get_user_id(login_token, "login", True)
+
     def _generate_base_macaroon(self, user_id):
         macaroon = pymacaroons.Macaroon(
             location=self.hs.config.server_name,
@@ -402,6 +421,57 @@ class AuthHandler(BaseHandler):
         macaroon.add_first_party_caveat("user_id = %s" % (user_id,))
         return macaroon
 
+    def _validate_macaroon_and_get_user_id(self, macaroon_str,
+                                           macaroon_type, validate_expiry):
+        try:
+            macaroon = pymacaroons.Macaroon.deserialize(macaroon_str)
+            user_id = self._get_user_from_macaroon(macaroon)
+            v = pymacaroons.Verifier()
+            v.satisfy_exact("gen = 1")
+            v.satisfy_exact("type = " + macaroon_type)
+            v.satisfy_exact("user_id = " + user_id)
+            if validate_expiry:
+                v.satisfy_general(self._verify_expiry)
+
+            v.verify(macaroon, self.hs.config.macaroon_secret_key)
+
+            v = pymacaroons.Verifier()
+            v.satisfy_general(self._verify_recognizes_caveats)
+            v.verify(macaroon, self.hs.config.macaroon_secret_key)
+            return user_id
+        except (pymacaroons.exceptions.MacaroonException, TypeError, ValueError):
+            raise AuthError(
+                self.INVALID_TOKEN_HTTP_STATUS, "Invalid token",
+                errcode=Codes.UNKNOWN_TOKEN
+            )
+
+    def _get_user_from_macaroon(self, macaroon):
+        user_prefix = "user_id = "
+        for caveat in macaroon.caveats:
+            if caveat.caveat_id.startswith(user_prefix):
+                return caveat.caveat_id[len(user_prefix):]
+        raise AuthError(
+            self.INVALID_TOKEN_HTTP_STATUS, "No user_id found in token",
+            errcode=Codes.UNKNOWN_TOKEN
+        )
+
+    def _verify_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 _verify_recognizes_caveats(self, caveat):
+        first_space = caveat.find(" ")
+        if first_space < 0:
+            return False
+        second_space = caveat.find(" ", first_space + 1)
+        if second_space < 0:
+            return False
+        return caveat[:second_space + 1] in self._KNOWN_CAVEAT_PREFIXES
+
     @defer.inlineCallbacks
     def set_password(self, user_id, newpassword):
         password_hash = self.hash(newpassword)