summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLars Wirzenius <liw@liw.fi>2018-10-20 18:51:38 +0300
committerLars Wirzenius <liw@liw.fi>2018-10-20 19:25:58 +0300
commit2d76d3425b2c58957d9317110bd09974e07706bb (patch)
tree106654ab03324e63374afe66bb9a5bb103721dfb
parentd44549b5b66d6900cc0e929f89e01d35884e2dcc (diff)
downloadmuck-poc-2d76d3425b2c58957d9317110bd09974e07706bb.tar.gz
Add: TokenChecker
-rw-r--r--muck/__init__.py2
-rw-r--r--muck/token.py60
-rw-r--r--muck/token_tests.py114
3 files changed, 176 insertions, 0 deletions
diff --git a/muck/__init__.py b/muck/__init__.py
index d14c8e1..a9e81e7 100644
--- a/muck/__init__.py
+++ b/muck/__init__.py
@@ -24,3 +24,5 @@ from .change import (
from .mem import MemoryStore
from .pers import PersistentStore
from .store import Store
+
+from .token import TokenChecker, create_token
diff --git a/muck/token.py b/muck/token.py
new file mode 100644
index 0000000..a561632
--- /dev/null
+++ b/muck/token.py
@@ -0,0 +1,60 @@
+# Copyright (C) 2018 Lars Wirzenius
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
+import Crypto.PublicKey.RSA
+import jwt
+
+import muck
+
+
+class TokenChecker:
+
+ def __init__(self, signing_key_pub):
+ pubkey = Crypto.PublicKey.RSA.importKey(signing_key_pub)
+ self._key = pubkey.exportKey('OpenSSH')
+
+ def parse_header(self, value):
+ token = self._get_token_text(value)
+ options = {
+ 'verify_aud': False,
+ }
+ try:
+ return jwt.decode(
+ token, key=self._key, audience=None, options=options)
+ except jwt.DecodeError as e:
+ raise muck.Error(str(e))
+
+ def _get_token_text(self, value):
+ if not isinstance(value, str):
+ raise muck.Error('Header does not have a string value')
+
+ if not value:
+ raise muck.Error('Header does not have a non-empty string value')
+
+ words = value.split()
+
+ if len(words) != 2:
+ raise muck.Error('Header does not consist of two words')
+
+ if words[0].lower() != 'bearer':
+ raise muck.Error('Header does not start with "Bearer"')
+
+ return words[1]
+
+
+def create_token(claims, key_text):
+ key = Crypto.PublicKey.RSA.importKey(key_text)
+ token = jwt.encode(claims, key.exportKey('PEM'), algorithm='RS512')
+ return token.decode('ascii')
diff --git a/muck/token_tests.py b/muck/token_tests.py
new file mode 100644
index 0000000..8dd7199
--- /dev/null
+++ b/muck/token_tests.py
@@ -0,0 +1,114 @@
+# Copyright (C) 2018 Lars Wirzenius
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
+import time
+import unittest
+
+import muck
+
+
+key_text = '''\
+-----BEGIN RSA PRIVATE KEY-----
+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
+-----END RSA PRIVATE KEY-----
+'''
+
+
+class TokenCheckerTests(unittest.TestCase):
+
+ def setUp(self):
+ self.tc = muck.TokenChecker(key_text.strip().encode('ascii'))
+
+ def test_rejects_no_authorization_header(self):
+ with self.assertRaises(muck.Error):
+ self.tc.parse_header(None)
+
+ def test_rejects_empty_authorization_header(self):
+ with self.assertRaises(muck.Error):
+ self.tc.parse_header('')
+
+ def test_rejects_nonbearer_header(self):
+ with self.assertRaises(muck.Error):
+ self.tc.parse_header('Foo')
+
+ def test_rejects_nonbearer_header_2(self):
+ with self.assertRaises(muck.Error):
+ self.tc.parse_header('Foo XXX')
+
+ def test_rejects_bad_bearer_header(self):
+ with self.assertRaises(muck.Error):
+ self.tc.parse_header('Bearer XXX')
+
+ def test_accepts_valid_token(self):
+ claims = {
+ 'sub': 'subject-1',
+ 'scopes': 'scope-1',
+ 'iss': 'issuer-1',
+ 'aud': 'audience-1',
+ 'exp': time.time() + 3600,
+ }
+
+ token = muck.create_token(claims, key_text)
+ header = 'Bearer {}'.format(token)
+ parsed = self.tc.parse_header(header)
+ self.assertEqual(claims, parsed)