summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLars Wirzenius <liw@liw.fi>2018-01-30 12:59:32 +0200
committerLars Wirzenius <liw@liw.fi>2018-01-30 12:59:32 +0200
commit6e434e97f18520aa55d7d1354fa1a63925ba913e (patch)
tree21fb4aaef08f0f37552380a3d101c828f01c5134
parent57556c868874aabe1a3d09526b44b8bf9c750ba5 (diff)
downloadqvisqve-6e434e97f18520aa55d7d1354fa1a63925ba913e.tar.gz
Add: token generation and API routes
-rw-r--r--salami/token.py72
-rw-r--r--salami/token_router.py107
-rw-r--r--without-tests2
3 files changed, 181 insertions, 0 deletions
diff --git a/salami/token.py b/salami/token.py
new file mode 100644
index 0000000..953318b
--- /dev/null
+++ b/salami/token.py
@@ -0,0 +1,72 @@
+#!/usr/bin/python3
+# Copyright (C) 2017-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 Crypto.PublicKey.RSA
+import jwt
+
+
+import salami
+
+
+class TokenGenerator:
+
+ _algorithm = 'RS512'
+
+ def __init__(self):
+ self._issuer = None
+ self._lifetime = None
+ self._key = None
+
+ def set_issuer(self, issuer):
+ self._issuer = issuer
+ salami.log.log('info', msg_text='Set issuer', issuer=issuer)
+
+ def set_lifetime(self, lifetime):
+ self._lifetime = lifetime
+ salami.log.log(
+ 'info', msg_text='Set token lifetime', lifetime=lifetime)
+
+ def set_signing_key(self, key):
+ imported_key = Crypto.PublicKey.RSA.importKey(key)
+ self._key = imported_key.exportKey('PEM')
+ salami.log.log(
+ 'info', msg_text='Set signing key', key=self._key,
+ orig_key=key, imported_key=imported_key)
+
+ def new_token(self, audience, scope):
+ assert self._issuer is not None
+ assert self._lifetime is not None
+ assert self._key is not None
+
+ now = time.time()
+ claims = {
+ 'iss': self._issuer,
+ 'sub': '',
+ 'aud': audience,
+ 'exp': now + self._lifetime,
+ 'scope': scope,
+ }
+
+ token = jwt.encode(
+ claims,
+ self._key,
+ algorithm=self._algorithm)
+
+ return token.decode('ascii')
diff --git a/salami/token_router.py b/salami/token_router.py
new file mode 100644
index 0000000..b7ababb
--- /dev/null
+++ b/salami/token_router.py
@@ -0,0 +1,107 @@
+# 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 urllib.parse
+
+
+import bottle
+
+
+import salami
+
+
+class TokenRouter(salami.Router):
+
+ def __init__(self, token_generator, clients):
+ super().__init__()
+ self._generator = token_generator
+ self._clients = Clients(clients)
+
+ def get_routes(self):
+ return [
+ {
+ 'method': 'POST',
+ 'path': '/token',
+ 'callback': self._create_token,
+ 'needs-authorization': False,
+ },
+ ]
+
+ def _create_token(self, content_type, body, **kwargs):
+ salami.log.log('xxx', body=body, kwargs=kwargs)
+
+ if content_type != 'application/x-www-form-urlencoded':
+ return salami.bad_request_response('Wrong content type')
+
+ client_id, client_secret = bottle.request.auth
+ if not self._clients.is_correct_secret(client_id, client_secret):
+ return salami.unauthorized_response('Unauthorized')
+
+ params = self._get_form_params(body)
+
+ grant_type = self._get_grant_type(params)
+ if grant_type != 'client_credentials':
+ return salami.bad_request_response('Wrong grant type')
+
+ scope = self._get_scope(params)
+ if scope is None:
+ return salami.bad_request_response('Bad scope')
+
+ allowed = self._clients.get_allowed_scopes(client_id)
+ scope = ' '.join(
+ s
+ for s in scope.split()
+ if s in allowed
+ )
+
+ token = self._generator.new_token(client_id, scope)
+ return salami.ok_response({
+ 'access_token': token,
+ 'token_type': 'bearer',
+ 'scope': scope,
+ })
+
+ def _get_form_params(self, body):
+ body = body.decode('UTF-8')
+ return urllib.parse.parse_qs(body)
+
+ def _get_grant_type(self, params):
+ grant_type = params.get('grant_type')
+ if len(grant_type) == 1:
+ return grant_type[0]
+ return None
+
+ def _get_scope(self, params):
+ scope = params.get('scope', [])
+ if len(scope) > 1:
+ return None
+ elif len(scope) == 0:
+ return ''
+ else:
+ return scope[0]
+
+
+class Clients:
+
+ def __init__(self, clients):
+ self._clients = clients
+
+ def is_correct_secret(self, client_id, secret):
+ return (client_id in self._clients and
+ self._clients[client_id].get('client_secret') == secret)
+
+ def get_allowed_scopes(self, client_id):
+ return self._clients[client_id].get('allowed_scopes', [])
diff --git a/without-tests b/without-tests
index 58eaf40..a95a008 100644
--- a/without-tests
+++ b/without-tests
@@ -6,6 +6,8 @@ salami/backend.py
salami/log_setup.py
salami/responses.py
salami/router.py
+salami/token.py
+salami/token_router.py
salami/version.py
salami/version_router.py
yarns/lib.py