From e6d0ac5d6d5f505c263dc3ccf4811f80b6c4dc07 Mon Sep 17 00:00:00 2001 From: Victor Fernandez de Alba Date: Tue, 21 Feb 2012 21:25:49 +0100 Subject: [PATCH] tests passed for endpoint token --- .gitignore | 3 +- README.rst | 137 +++++++++++++++++++++++++++++++++++++++ README.txt | 1 - osiris/authorization.py | 3 +- osiris/tests.py | 25 ------- osiris/tests/__init__.py | 0 osiris/tests/passwd | 1 + osiris/tests/test.ini | 24 +++++++ osiris/tests/tests.py | 46 +++++++++++++ osiris/tests/who.ini | 51 +++++++++++++++ setup.py | 6 +- who.ini | 2 +- 12 files changed, 267 insertions(+), 32 deletions(-) create mode 100644 README.rst delete mode 100644 README.txt delete mode 100644 osiris/tests.py create mode 100644 osiris/tests/__init__.py create mode 100644 osiris/tests/passwd create mode 100644 osiris/tests/test.ini create mode 100644 osiris/tests/tests.py create mode 100644 osiris/tests/who.ini diff --git a/.gitignore b/.gitignore index 129ad9e..244baab 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ *.log *.pyc *.egg-info -*.DS_Store \ No newline at end of file +*.DS_Store +.codeintel \ No newline at end of file diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..a29e228 --- /dev/null +++ b/README.rst @@ -0,0 +1,137 @@ +Introduction +------------ +Osiris (/oʊˈsaɪərɨs/) is an Egyptian god, usually identified as the god of the afterlife, the underworld and the dead. He is classically depicted as a green-skinned man with a pharaoh's beard, partially mummy-wrapped at the legs, wearing a distinctive crown with two large ostrich feathers at either side, and holding a symbolic crook and flail. Osiris was the afterlife's judge, he weighed the dead souls and compare them with the Feather of Truth. Those which weighed the most were sent to Ammut (the soul devourer) and not heavy enough to Aaru (the egyptian paradise). + +Osiris is an oAuth 2.0 (draft 22) compliant server based on Pyramid. The current version (1.0) it supports the `Resource owner password credentials` authentication flow. It uses pyramid_who as user backend providing the way to behave as an oAuth authentication gateway. This means that you can use your authentication backend (LDAP, SQL, etc.) oAuth enabled with Osiris. Osiris uses a pluggable store factory to store the issued token information. The current version includes the MongoDB one. + +The `Resource owner password credentials` flow +---------------------------------------------- +This flow is not the most popular oAuth flow, but it's useful in case that we want to oAuth enable an app or a set of apps in an scenario with an already existing user backend. Using this flow you can use Osiris as a gateway between your existing user store and oAuth enable it. Osiris will authenticate the user credentials against your user store and if suceeds it will issue a oAuth token. Then, an app can use it to impersonate the user's token to access an oAuth enabled REST API, for example. + +For that reason and out of the oAuth specification, Osiris features an additional endpoint to allow remote applications and resource servers to check previously issued tokens and users and validate it. This endpoint will respond if the token is valid for the user specified and if the token is not expired or revoked. + +You can use Osiris as a standalone application or use it as a Pyramid plugin and make your app Osiris enabled. + +Setup +----- + +This is the configuration to use it as a standalone Pyramid app, along with your own one using Paste urlmap in your app .ini: + +.. code-block:: ini + + [server:main] + use = egg:Paste#http + host = 0.0.0.0 + port = 80 + + [composite:main] + use = egg:Paste#urlmap + / = YOURAPP + /oauth2 = osiris + + [app:osiris] + use = egg:osiris + + osiris.store = osiris.store.mongodb_store + osiris.store.host = localhost + osiris.store.port = 27017 + osiris.store.db = osiris + osiris.store.collection = tokens + osiris.tokenexpiry = 0 + + osiris.whoconfig = %(here)s/who.ini + + [app:YOURAPP] + use = egg:YOURAPP + full_stack = true + static_files = true + +You can also Osiris enable your own app, in your __init__.py:: + + config.include(osiris) + +and in the .ini: + +.. code-block:: ini + + osiris.store = osiris.store.mongodb_store + osiris.store.host = localhost + osiris.store.port = 27017 + osiris.store.db = osiris + osiris.store.collection = tokens + osiris.tokenexpiry = 0 + + osiris.whoconfig = %(here)s/who.ini + +Options +------- +These are the .ini options available for Osiris: + +osiris.store + Currently only available ``osiris.store.mongodb_store``. Required. + +osiris.store.host + Defaults to 'localhost'. Optional. + +osiris.store.port + Defaults to '27017'. Optional. + +osiris.store.db + The name of the database. Defaults to 'osiris'. Optional. + +osiris.store.collection + The collection to store the tokens. Defaults to 'tokens'. Optional. + +osiris.tokenexpiry + The time in seconds that the token is valid. Defaults to 0 (unlimited). Optional. + +osiris.whoconfig + The pyramid_who (repoze.who) .ini with the configuration of the authentication backends. Required. + +REST API for `Resource owner password credentials` flow +------------------------------------------------------- +Following the oAuth 2.0 authentication standard (draft 22), the `Resource owner password credentials` flow must implement this web services and use these parameters: + +/token + Method: + POST + + Params: + grant_type + Required. Value must be set to password + + username + Required. The resource owner username, encoded as UTF-8. + + password + Required. The resource owner password, encoded as UTF-8. + + scope + Optional. The scope of the access request. + + Content-Type: + application/x-www-form-urlencoded + + Response: + HTTP/1.1 200 OK + Content-Type: application/json;charset=UTF-8 + Cache-Control: no-store + Pragma: no-cache + + { + "access_token":"2YotnFZFEjr1zCsicMWpAA", + "token_type":"bearer", + "expires_in":3600, + "scope": "exampleScope" + } + +To do +----- +Osiris features only one oAuth 2.0 authentication flow, the `Resource owner password credentials`. It's ready to accomodate the remaining flows defined by oAuth 2.0. A similar case happens with the available storage backends. The current version sports only the MongoDB storage but Osiris support the use of a plugin storage model and can accomodate more storage types. + +Of course, any contribution is welcome. Please, feel free to contribute with your own storage plugins and help implementing the remaining oAuth flows. + +Credits +------- +Pluggable store factory inspired by Ben Bangert's Velruse (https://github.com/bbangert/velruse). +Borrowed error handling from pyramid-oauth2 (http://code.google.com/p/pyramid-oauth2/) by Kevin Van Wilder et al. diff --git a/README.txt b/README.txt deleted file mode 100644 index 23cfaed..0000000 --- a/README.txt +++ /dev/null @@ -1 +0,0 @@ -osiris README diff --git a/osiris/authorization.py b/osiris/authorization.py index 276acbc..997c744 100644 --- a/osiris/authorization.py +++ b/osiris/authorization.py @@ -23,7 +23,8 @@ def password_authorization(request, username, password, scope, expires_in): # Issue token if stored: return dict( - token=token, + access_token=token, + token_type='bearer', scope=scope, expires_in=expires_in ) diff --git a/osiris/tests.py b/osiris/tests.py deleted file mode 100644 index 9ea2320..0000000 --- a/osiris/tests.py +++ /dev/null @@ -1,25 +0,0 @@ -import unittest -import os -from pyramid import testing -from paste.deploy import loadapp - - -class osirisTests(unittest.TestCase): - def setUp(self): - conf_dir = os.path.abspath(__file__ + '/../..') - app = loadapp('config:development.ini', relative_to=conf_dir) - from webtest import TestApp - self.testapp = TestApp(app) - - def tearDown(self): - testing.tearDown() - - def test_token_endpoint(self): - res = self.testapp.post('/token?grant_type=password&username=victor&password=1', status=200) - - - # def test_my_view(self): - # from osiris.views import my_view - # request = testing.DummyRequest() - # info = my_view(request) - # self.assertEqual(info['project'], 'osiris') diff --git a/osiris/tests/__init__.py b/osiris/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/osiris/tests/passwd b/osiris/tests/passwd new file mode 100644 index 0000000..cf178d3 --- /dev/null +++ b/osiris/tests/passwd @@ -0,0 +1 @@ +testuser:GAQ6kCHXfoiog diff --git a/osiris/tests/test.ini b/osiris/tests/test.ini new file mode 100644 index 0000000..d4a38b1 --- /dev/null +++ b/osiris/tests/test.ini @@ -0,0 +1,24 @@ +[app:main] +use = egg:osiris + +pyramid.reload_templates = true +pyramid.debug_authorization = false +pyramid.debug_notfound = false +pyramid.debug_routematch = false +pyramid.debug_templates = true +pyramid.default_locale_name = en +pyramid.includes = pyramid_debugtoolbar + +osiris.store = osiris.store.mongodb_store +osiris.store.host = localhost +osiris.store.port = 27017 +osiris.store.db = osiris +osiris.store.collection = tokens +osiris.tokenexpiry = 0 + +osiris.whoconfig = %(here)s/who.ini + +[server:main] +use = egg:Paste#http +host = 0.0.0.0 +port = 6543 \ No newline at end of file diff --git a/osiris/tests/tests.py b/osiris/tests/tests.py new file mode 100644 index 0000000..822c3b5 --- /dev/null +++ b/osiris/tests/tests.py @@ -0,0 +1,46 @@ +import unittest +import os +from pyramid import testing +from paste.deploy import loadapp +import pymongo + +class osirisTests(unittest.TestCase): + def setUp(self): + conf_dir = os.path.dirname(__file__) + self.app = loadapp('config:test.ini', relative_to=conf_dir) + from webtest import TestApp + self.testapp = TestApp(self.app) + + def tearDown(self): + testing.tearDown() + + def test_token_endpoint(self): + testurl = '/token?grant_type=password&username=testuser&password=test' + resp = self.testapp.post(testurl, status=200) + response = resp.json + self.assertTrue('access_token' in response and len(response.get('access_token')) == 20) + self.assertTrue('token_type' in response and response.get('token_type') == 'bearer') + self.assertTrue('scope' in response and response.get('scope') == '') + self.assertTrue('expires_in' in response and response.get('expires_in') == '0') + self.assertEqual(resp.content_type, 'application/json') + + def test_token_endpoint_autherror(self): + # Not the password + testurl = '/token?grant_type=password&username=testuser&password=notthepassword' + resp = self.testapp.post(testurl, status=401) + self.assertEqual(resp.content_type, 'application/json') + + # No such user + testurl = '/token?grant_type=password&username=nosuchuser&password=notthepassword' + resp = self.testapp.post(testurl, status=401) + self.assertEqual(resp.content_type, 'application/json') + + def test_token_storage(self): + testurl = '/token?grant_type=password&username=testuser&password=test' + resp = self.testapp.post(testurl, status=200) + response = resp.json + + token_store = self.app.registry.osiris_store.retrieve(response.get('access_token')) + self.assertTrue(token_store) + self.assertEqual(token_store.get('token'), response.get('access_token')) + self.assertEqual(token_store.get('username'), 'testuser') diff --git a/osiris/tests/who.ini b/osiris/tests/who.ini new file mode 100644 index 0000000..ed542f4 --- /dev/null +++ b/osiris/tests/who.ini @@ -0,0 +1,51 @@ +[plugin:redirform] +# identification and challenge +use = repoze.who.plugins.redirector:make_plugin +login_url = /login + +[plugin:basicauth] +# identification and challenge +use = repoze.who.plugins.basicauth:make_plugin +realm = 'OSIRIS' + +[plugin:auth_tkt] +# identification +use = repoze.who.plugins.auth_tkt:make_plugin +secret = sEEkr1t +cookie_name = chocolate +secure = False +include_ip = False + +[plugin:htpasswd] +# authentication +use = repoze.who.plugins.htpasswd:make_plugin +filename = %(here)s/passwd +check_fn = repoze.who.plugins.htpasswd:crypt_check + +[general] +request_classifier = repoze.who.classifiers:default_request_classifier +challenge_decider = repoze.who.classifiers:default_challenge_decider + +[identifiers] +# plugin_name;classifier_name:.. or just plugin_name (good for any) +plugins = + auth_tkt + basicauth + +[authenticators] +# plugin_name;classifier_name.. or just plugin_name (good for any) +plugins = + auth_tkt + htpasswd + +[challengers] +# plugin_name;classifier_name:.. or just plugin_name (good for any) +plugins = + redirform;browser + basicauth + +# Metadata providers +[mdproviders] +plugins = + + diff --git a/setup.py b/setup.py index c28c24e..820a50f 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ from setuptools import setup, find_packages here = os.path.abspath(os.path.dirname(__file__)) -README = open(os.path.join(here, 'README.txt')).read() +README = open(os.path.join(here, 'README.rst')).read() CHANGES = open(os.path.join(here, 'CHANGES.txt')).read() requires = ['pyramid', 'pyramid_debugtoolbar', 'pyramid_who', 'pymongo'] @@ -11,7 +11,7 @@ setup(name='osiris', version='1.0', description='Pyramid based oAuth server', - long_description=README + '\n\n' + CHANGES, + long_description=README + '\n\n' + CHANGES, classifiers=[ "Programming Language :: Python", "Framework :: Pylons", @@ -28,7 +28,7 @@ install_requires=requires, tests_require=requires + ['WebTest'], test_suite="osiris", - entry_points = """\ + entry_points="""\ [paste.app_factory] main = osiris:make_osiris_app """, diff --git a/who.ini b/who.ini index 482b539..ed542f4 100644 --- a/who.ini +++ b/who.ini @@ -6,7 +6,7 @@ login_url = /login [plugin:basicauth] # identification and challenge use = repoze.who.plugins.basicauth:make_plugin -realm = 'EPI' +realm = 'OSIRIS' [plugin:auth_tkt] # identification