From 2d76d3425b2c58957d9317110bd09974e07706bb Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Sat, 20 Oct 2018 18:51:38 +0300 Subject: Add: TokenChecker --- muck/__init__.py | 2 + muck/token.py | 60 +++++++++++++++++++++++++++ muck/token_tests.py | 114 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 176 insertions(+) create mode 100644 muck/token.py create mode 100644 muck/token_tests.py 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 . + + +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 . + + +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) -- cgit v1.2.1