From df351c2661cd838e1b7de521f1fcd4f84acdf4a0 Mon Sep 17 00:00:00 2001 From: Lars Wirzenius Date: Mon, 5 Nov 2018 09:56:36 +0200 Subject: Add: sub fields to clients, tokens created by client-cred grant --- NEWS | 5 +++++ qvisqve/authn_entity_manager.py | 9 +++++++++ qvisqve/authn_entity_manager_tests.py | 15 +++++++++++++++ qvisqve/token_router.py | 3 ++- yarns/200-client-creds.yarn | 29 +++++++++++++++++++++++++++++ yarns/900-local.yarn | 4 ++++ yarns/lib.py | 1 + 7 files changed, 65 insertions(+), 1 deletion(-) diff --git a/NEWS b/NEWS index d6729ac..cd65c84 100644 --- a/NEWS +++ b/NEWS @@ -24,6 +24,11 @@ Version 0.10+git, not yet released * Change: the HTML code for the login form is now in a separate file, so it is easier to style. +* API clients may now have a `sub` field, and if they do, tokens + created using the client credentials grant variant of OAuth2 get + their `sub` claim set accordingly. This is a first step towards + allowing users to authorize API clients to act on their behalf. + Version 0.9, released 2018-02-09 --------------------------------- diff --git a/qvisqve/authn_entity_manager.py b/qvisqve/authn_entity_manager.py index f84634c..e3d454a 100644 --- a/qvisqve/authn_entity_manager.py +++ b/qvisqve/authn_entity_manager.py @@ -71,6 +71,15 @@ class ClientManager(AuthenticatingEntityManager): def __init__(self, rs): super().__init__(rs, 'client') + def get_subject(self, username): + user = self.get(username) + return user.get('sub') + + def set_subject(self, username, sub): + user = self.get(username) + user['sub'] = sub + self.create(username, user) + class UserManager(AuthenticatingEntityManager): diff --git a/qvisqve/authn_entity_manager_tests.py b/qvisqve/authn_entity_manager_tests.py index 0d51d4b..579791f 100644 --- a/qvisqve/authn_entity_manager_tests.py +++ b/qvisqve/authn_entity_manager_tests.py @@ -94,6 +94,21 @@ class ClientManagerTests(unittest.TestCase): self.cm.set_secret(client['id'], secret) self.assertTrue(self.cm.is_valid_secret(client['id'], secret)) + def test_has_no_subject_initially(self): + client = { + 'id': 'test-client', + } + self.cm.create(client['id'], client) + self.assertEqual(self.cm.get_subject(client['id']), None) + + def test_sets_subject(self): + client = { + 'id': 'test-client', + } + self.cm.create(client['id'], client) + self.cm.set_subject(client['id'], 'tomjon') + self.assertEqual(self.cm.get_subject(client['id']), 'tomjon') + class UserManagerTests(unittest.TestCase): diff --git a/qvisqve/token_router.py b/qvisqve/token_router.py index 4778063..bd3744c 100644 --- a/qvisqve/token_router.py +++ b/qvisqve/token_router.py @@ -106,7 +106,8 @@ class ClientCredentialsGrant(Grant): if s in allowed ) - token = self._generator.new_token(client_id, scope) + sub = self._clients.get_subject(client_id) + token = self._generator.new_token(client_id, scope, subject_id=sub) return qvisqve.ok_response({ 'access_token': token, 'token_type': 'Bearer', diff --git a/yarns/200-client-creds.yarn b/yarns/200-client-creds.yarn index 9eff22a..418e9b9 100644 --- a/yarns/200-client-creds.yarn +++ b/yarns/200-client-creds.yarn @@ -102,3 +102,32 @@ scopes, as described above. AND token expires in an hour FINALLY Qvisqve is stopped + +API client tied to a subject +----------------------------------------------------------------------------- + + SCENARIO get token using client credentials tied to a subject + + GIVEN an API client "bigco" + AND API client has secret "secrit" + AND API client has allowed scopes "read write" + AND API client has subject "tomjon" + + AND a Qvisqve configuration for "https://qvisqve.example.com" + AND Qvisqve configuration has a token lifetime of 3600 + AND a running Qvisqve instance + + WHEN client requests POST /token + ... with client_id "bigco", client_secret "secrit", and + ... scopes "read write delete" + + THEN HTTP status code is 200 OK + AND Content-Type is application/json + AND body is a correctly signed JWT token + AND token has claim iss as "https://qvisqve.example.com" + AND token has claim sub as "tomjon" + AND token has claim aud as "bigco" + AND token has claim scope as "read write" + AND token expires in an hour + + FINALLY Qvisqve is stopped diff --git a/yarns/900-local.yarn b/yarns/900-local.yarn index cae5db1..276e656 100644 --- a/yarns/900-local.yarn +++ b/yarns/900-local.yarn @@ -31,6 +31,10 @@ along with this program. If not, see . scopes = get_next_match() V['allowed_scopes'] = scopes.split() + IMPLEMENTS GIVEN API client has subject "(.+)" + sub = get_next_match() + V['sub'] = sub + IMPLEMENTS GIVEN a Qvisqve configuration for "(.+)" V['iss'] = get_next_match() diff --git a/yarns/lib.py b/yarns/lib.py index a9ba526..9d57143 100644 --- a/yarns/lib.py +++ b/yarns/lib.py @@ -190,6 +190,7 @@ def start_qvisqve(): client = { 'hashed_secret': sh.hash(V['client_secret']), 'allowed_scopes': V['allowed_scopes'], + 'sub': V['sub'], } filename = os.path.join(store, 'client', V['client_id']) -- cgit v1.2.1