summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLars Wirzenius <liw@liw.fi>2018-07-13 15:21:14 +0300
committerLars Wirzenius <liw@liw.fi>2018-07-13 15:21:14 +0300
commit2ee4809524a80e2223049e10b67d8d6e068d4285 (patch)
tree9da7a25863979c8f5554b98e464f96c4643b4aee
parent5e68c02a915ff50a9b8bc14b7f68c255be92cf21 (diff)
parentda30b04924a2748e059a4fbc98eb6e6d0d0177f4 (diff)
downloadqvisqve-2ee4809524a80e2223049e10b67d8d6e068d4285.tar.gz
Merge: end-user login implementation
-rw-r--r--qvisqve/__init__.py3
-rw-r--r--qvisqve/api.py2
-rw-r--r--qvisqve/auth_router.py51
-rw-r--r--qvisqve/login_router.py62
-rw-r--r--qvisqve/responses.py9
-rw-r--r--qvisqve/token_router.py75
-rw-r--r--views/login.tpl18
-rw-r--r--without-tests2
-rw-r--r--yarns/300-end-user-auth.yarn6
-rw-r--r--yarns/900-implements.yarn6
-rw-r--r--yarns/900-local.yarn10
11 files changed, 216 insertions, 28 deletions
diff --git a/qvisqve/__init__.py b/qvisqve/__init__.py
index a1d80f9..32032b6 100644
--- a/qvisqve/__init__.py
+++ b/qvisqve/__init__.py
@@ -19,12 +19,15 @@ from .responses import (
created_response,
ok_response,
unauthorized_response,
+ found_response,
)
from .log_setup import setup_logging, log
from .token import TokenGenerator
from .router import Router
from .version_router import VersionRouter
+from .login_router import LoginRouter
+from .auth_router import AuthRouter
from .token_router import TokenRouter
from .api import API
diff --git a/qvisqve/api.py b/qvisqve/api.py
index 2dee954..a6d71ef 100644
--- a/qvisqve/api.py
+++ b/qvisqve/api.py
@@ -27,6 +27,8 @@ class API:
routers = [
qvisqve.VersionRouter(),
+ qvisqve.LoginRouter(),
+ qvisqve.AuthRouter(self._config.get('callback_url', 'localhost')),
qvisqve.TokenRouter(
self._create_token_generator(), self._get_clients()),
]
diff --git a/qvisqve/auth_router.py b/qvisqve/auth_router.py
new file mode 100644
index 0000000..d4f3ca6
--- /dev/null
+++ b/qvisqve/auth_router.py
@@ -0,0 +1,51 @@
+# Copyright (C) 2018 Ivan Dolgov
+#
+# 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 qvisqve
+
+
+class AuthRouter(qvisqve.Router):
+
+ def __init__(self, callback_url):
+ super().__init__()
+ self._callback_url = callback_url
+
+ def get_routes(self):
+ return [
+ {
+ 'method': 'POST',
+ 'path': '/auth',
+ 'callback': self._auth,
+ 'needs-authorization': False,
+ },
+ ]
+
+ def _auth(self, content_type, body, *args, **kwargs):
+ if content_type != 'application/x-www-form-urlencoded':
+ return qvisqve.bad_request_response('Wrong content type')
+
+ # TODO:
+ # - perform actual auth
+ # - create and store auth code
+ # - use callback url provided in request
+
+ params = urllib.parse.urlencode({'code': 123})
+ url = '{}?{}'.format(self._callback_url, params)
+
+ return qvisqve.found_response('Redirect to callback url', url)
diff --git a/qvisqve/login_router.py b/qvisqve/login_router.py
new file mode 100644
index 0000000..ef460b4
--- /dev/null
+++ b/qvisqve/login_router.py
@@ -0,0 +1,62 @@
+# Copyright (C) 2018 Ivan Dolgov
+#
+# 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 bottle
+
+
+import qvisqve
+
+
+login_template = '''\
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <meta name="description" content="Qvisqve Login">
+ <meta name="author" content="Qvarnlabs Ltd">
+ <title>Qvisqve Login</title>
+ </head>
+ <body>
+ <form action="/auth" method="POST">
+ User name: <input name="username" type="text" />
+ <br />
+ Password: <input name="password" type="password" />
+ <br />
+ <input type="submit" value="Login" />
+ </form>
+ </body>
+</html>
+'''
+
+
+class LoginRouter(qvisqve.Router):
+
+ def get_routes(self):
+ return [
+ {
+ 'method': 'GET',
+ 'path': '/login',
+ 'callback': self._login,
+ 'needs-authorization': False,
+ },
+ ]
+
+ def _login(self, *args, **kwargs):
+ headers = {
+ 'Content-Type': 'text/html; charset=utf-8',
+ }
+ template = bottle.template(login_template)
+ return qvisqve.ok_response(template, headers=headers)
diff --git a/qvisqve/responses.py b/qvisqve/responses.py
index 281ec43..d36094e 100644
--- a/qvisqve/responses.py
+++ b/qvisqve/responses.py
@@ -57,3 +57,12 @@ def unauthorized_response(body):
'Content-Type': 'text/plain',
}
return response(apifw.HTTP_UNAUTHORIZED, body, headers)
+
+
+def found_response(body, location):
+ headers = {
+ 'Content-Type': 'text/plain',
+ 'Location': location,
+ }
+ # TODO: use apifw.FOUND_CREATED when available
+ return response(302, body, headers)
diff --git a/qvisqve/token_router.py b/qvisqve/token_router.py
index 9acc924..b5f30fe 100644
--- a/qvisqve/token_router.py
+++ b/qvisqve/token_router.py
@@ -28,8 +28,11 @@ class TokenRouter(qvisqve.Router):
def __init__(self, token_generator, clients):
super().__init__()
- self._generator = token_generator
- self._clients = Clients(clients)
+ args = (Clients(clients), token_generator)
+ self._grants = {
+ 'client_credentials': ClientCredentialsGrant(*args),
+ 'authorization_code': AuthorizationCodeGrant(*args),
+ }
def get_routes(self):
return [
@@ -47,15 +50,40 @@ class TokenRouter(qvisqve.Router):
if content_type != 'application/x-www-form-urlencoded':
return qvisqve.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 qvisqve.unauthorized_response('Unauthorized')
-
params = self._get_form_params(body)
-
grant_type = self._get_grant_type(params)
- if grant_type != 'client_credentials':
+ grant = self._get_grant(grant_type)
+ if grant is None:
return qvisqve.bad_request_response('Wrong grant type')
+ return grant.get_token(bottle.request, params)
+
+ 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_grant(self, grant_type):
+ return self._grants.get(grant_type)
+
+
+class Grant:
+
+ def __init__(self, clients, generator):
+ self._clients = clients
+ self._generator = generator
+
+
+class ClientCredentialsGrant(Grant):
+
+ def get_token(self, request, params):
+ client_id, client_secret = request.auth
+ if not self._clients.is_correct_secret(client_id, client_secret):
+ return qvisqve.unauthorized_response('Unauthorized')
scope = self._get_scope(params)
if scope is None:
@@ -75,16 +103,6 @@ class TokenRouter(qvisqve.Router):
'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:
@@ -94,6 +112,27 @@ class TokenRouter(qvisqve.Router):
return ''
+class AuthorizationCodeGrant(Grant):
+
+ def get_token(self, request, params):
+ code = self._get_code(params)
+ # FIXME
+ if code is None or code != '123':
+ return qvisqve.unauthorized_response('Unauthorized')
+ empty_token = self._generator.new_token('', '')
+ return qvisqve.ok_response({
+ 'access_token': empty_token,
+ 'token_type': 'bearer',
+ 'scope': '',
+ })
+
+ def _get_code(self, params):
+ code = params.get('code')
+ if len(code) == 1:
+ return code[0]
+ return None
+
+
class Clients:
def __init__(self, clients):
diff --git a/views/login.tpl b/views/login.tpl
new file mode 100644
index 0000000..dae2c4c
--- /dev/null
+++ b/views/login.tpl
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <meta name="description" content="Qvisqve Login">
+ <meta name="author" content="Qvarnlabs Ltd">
+ <title>Qvisqve Login</title>
+ </head>
+ <body>
+ <form action="/auth" method="POST">
+ User name: <input name="username" type="text" />
+ <br />
+ Password: <input name="password" type="password" />
+ <br />
+ <input type="submit" value="Login" />
+ </form>
+ </body>
+</html>
diff --git a/without-tests b/without-tests
index 45ab43b..1fc97eb 100644
--- a/without-tests
+++ b/without-tests
@@ -3,8 +3,10 @@ doc/build.py
qvisqve/__init__.py
qvisqve/api.py
qvisqve/app.py
+qvisqve/auth_router.py
qvisqve/backend.py
qvisqve/log_setup.py
+qvisqve/login_router.py
qvisqve/responses.py
qvisqve/router.py
qvisqve/token.py
diff --git a/yarns/300-end-user-auth.yarn b/yarns/300-end-user-auth.yarn
index 2b717c8..8a4d412 100644
--- a/yarns/300-end-user-auth.yarn
+++ b/yarns/300-end-user-auth.yarn
@@ -19,18 +19,18 @@ FIXME: Explain the login process here, with sequence diagram.
User goes to the login URL and gets a login page.
- WHEN browser requests GET https://qvisqve/login
+ WHEN browser requests GET /login
THEN HTTP status code is 200 OK
AND Content-Type is text/html
AND body has an HTML form with field username
AND body has an HTML form with field password
- WHEN browser requests POST https://qvisqve/auth, with form values
+ WHEN browser requests POST /qvisqve/auth, with form values
... username=tomjon and password=hunter2
THEN HTTP status code is 302 Found
AND Location header is https://facade/callback?code=123
- WHEN facade requests POST https://qvisqve/token, with
+ WHEN facade requests POST /qvisqve/token, with
... form values grant_type=authorization_code and code=123
THEN HTTP status code is 200 OK
AND Content-Type is application/json
diff --git a/yarns/900-implements.yarn b/yarns/900-implements.yarn
index 53f675a..e21d8d5 100644
--- a/yarns/900-implements.yarn
+++ b/yarns/900-implements.yarn
@@ -84,8 +84,8 @@ This chapter shows the scenario step implementations.
IMPLEMENTS WHEN browser requests GET (\S+)
# FIXME: This is a dummy implemantation, does not do anything real.
- V['status_code'] = 200
- V['headers'] = {'Content-Type': 'text/html'}
+ path = get_next_match()
+ V['status_code'], V['headers'], V['body'] = get(V['API_URL'] + path, {})
IMPLEMENTS WHEN browser requests POST (\S+)$
# FIXME: This is a dummy implemantation, does not do anything real.
@@ -178,7 +178,7 @@ This chapter shows the scenario step implementations.
IMPLEMENTS THEN Content-Type is (\S+)
wanted = get_next_match()
headers = V['headers']
- assertEqual(headers['Content-Type'], wanted)
+ assertEqual(headers['Content-Type'][:len(wanted)], wanted)
IMPLEMENTS THEN Location header is (\S+)
# FIXME: This is a dummy implemantation, does not do anything real.
diff --git a/yarns/900-local.yarn b/yarns/900-local.yarn
index 14c3937..ddce8f8 100644
--- a/yarns/900-local.yarn
+++ b/yarns/900-local.yarn
@@ -38,12 +38,14 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
V['lifetime'] = int(get_next_match())
IMPLEMENTS GIVEN Qvisqve configuration has user account (\S+) with password (\S+)
- # FIXME: This is a dummy implemantation, does not do anything real.
- pass
+ username = get_next_match()
+ password = get_next_match()
+ V['users'] = { username: password }
IMPLEMENTS GIVEN Qvisqve configuration has application (\S+) with callback url (\S+)
- # FIXME: This is a dummy implemantation, does not do anything real.
- pass
+ app = get_next_match()
+ callback = get_next_match()
+ V['applications'] = { app: callback }
## Authentication setup