diff options
author | Lars Wirzenius <liw@liw.fi> | 2018-01-30 12:59:32 +0200 |
---|---|---|
committer | Lars Wirzenius <liw@liw.fi> | 2018-01-30 12:59:32 +0200 |
commit | 6e434e97f18520aa55d7d1354fa1a63925ba913e (patch) | |
tree | 21fb4aaef08f0f37552380a3d101c828f01c5134 | |
parent | 57556c868874aabe1a3d09526b44b8bf9c750ba5 (diff) | |
download | qvisqve-6e434e97f18520aa55d7d1354fa1a63925ba913e.tar.gz |
Add: token generation and API routes
-rw-r--r-- | salami/token.py | 72 | ||||
-rw-r--r-- | salami/token_router.py | 107 | ||||
-rw-r--r-- | without-tests | 2 |
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 |