diff --git a/ckanext/oauth2/cli.py b/ckanext/oauth2/cli.py
new file mode 100644
index 0000000..5623b30
--- /dev/null
+++ b/ckanext/oauth2/cli.py
@@ -0,0 +1,13 @@
+# -*- coding: utf-8 -*-
+
+import click
+
+@click.group()
+def oauth2():
+ """Oauth2 management commands.
+ """
+ pass
+
+
+def get_commands():
+ return [oauth2]
diff --git a/ckanext/oauth2/constants.py b/ckanext/oauth2/constants.py
index 3fe7854..720aea4 100644
--- a/ckanext/oauth2/constants.py
+++ b/ckanext/oauth2/constants.py
@@ -1,3 +1,3 @@
CAME_FROM_FIELD = 'came_from'
-INITIAL_PAGE = '/dashboard'
+INITIAL_PAGE = '/'
REDIRECT_URL = 'oauth2/callback'
diff --git a/ckanext/oauth2/controller.py b/ckanext/oauth2/controller.py
index acd965b..7900030 100644
--- a/ckanext/oauth2/controller.py
+++ b/ckanext/oauth2/controller.py
@@ -18,16 +18,14 @@
# You should have received a copy of the GNU Affero General Public License
# along with OAuth2 CKAN Extension. If not, see .
-from __future__ import unicode_literals
-
import logging
-import constants
+from ckanext.oauth2 import constants
from ckan.common import session
import ckan.lib.helpers as helpers
import ckan.lib.base as base
import ckan.plugins.toolkit as toolkit
-import oauth2
+from ckanext.oauth2 import oauth2
from ckanext.oauth2.plugin import _get_previous_page
diff --git a/ckanext/oauth2/db.py b/ckanext/oauth2/db.py
index 013f58f..5ac410a 100644
--- a/ckanext/oauth2/db.py
+++ b/ckanext/oauth2/db.py
@@ -18,10 +18,12 @@
# along with OAuth2 CKAN Extension. If not, see .
import sqlalchemy as sa
+from sqlalchemy.ext.declarative import declarative_base
-UserToken = None
-
+Base = declarative_base()
+metadata = Base.metadata
+UserToken = None
def init_db(model):
global UserToken
diff --git a/ckanext/oauth2/oauth2.py b/ckanext/oauth2/oauth2.py
index 28a2724..6bc3adc 100644
--- a/ckanext/oauth2/oauth2.py
+++ b/ckanext/oauth2/oauth2.py
@@ -19,11 +19,10 @@
# along with OAuth2 CKAN Extension. If not, see .
-from __future__ import unicode_literals
-
import base64
import ckan.model as model
-import db
+from ckanext.oauth2.db import UserToken
+import ckanext.oauth2.db as db
import json
import logging
from six.moves.urllib.parse import urljoin
@@ -38,23 +37,24 @@
import jwt
-import constants
+from .constants import CAME_FROM_FIELD, REDIRECT_URL
+from flask import Flask, request, redirect, session, url_for, jsonify
+
log = logging.getLogger(__name__)
def generate_state(url):
- return b64encode(bytes(json.dumps({constants.CAME_FROM_FIELD: url})))
+ return b64encode(bytes(json.dumps({CAME_FROM_FIELD: url}).encode()))
def get_came_from(state):
- return json.loads(b64decode(state)).get(constants.CAME_FROM_FIELD, '/')
+ return json.loads(b64decode(state)).get(CAME_FROM_FIELD, '/')
REQUIRED_CONF = ("authorization_endpoint", "token_endpoint", "client_id", "client_secret", "profile_api_url", "profile_api_user_field", "profile_api_mail_field")
-
class OAuth2Helper(object):
def __init__(self):
@@ -79,10 +79,7 @@ def __init__(self):
self.profile_api_groupmembership_field = six.text_type(os.environ.get('CKAN_OAUTH2_PROFILE_API_GROUPMEMBERSHIP_FIELD', toolkit.config.get('ckan.oauth2.profile_api_groupmembership_field', ''))).strip()
self.sysadmin_group_name = six.text_type(os.environ.get('CKAN_OAUTH2_SYSADMIN_GROUP_NAME', toolkit.config.get('ckan.oauth2.sysadmin_group_name', ''))).strip()
- self.redirect_uri = urljoin(urljoin(toolkit.config.get('ckan.site_url', 'http://localhost:5000'), toolkit.config.get('ckan.root_path')), constants.REDIRECT_URL)
-
- # Init db
- db.init_db(model)
+ self.redirect_uri = urljoin(urljoin(toolkit.config.get('ckan.site_url', 'http://localhost:5000'), toolkit.config.get('ckan.root_path')), REDIRECT_URL)
missing = [key for key in REQUIRED_CONF if getattr(self, key, "") == ""]
if missing:
@@ -97,7 +94,7 @@ def challenge(self, came_from_url):
auth_url, _ = oauth.authorization_url(self.authorization_endpoint)
log.debug('Challenge: Redirecting challenge to page {0}'.format(auth_url))
# CKAN 2.6 only supports bytes
- return toolkit.redirect_to(auth_url.encode('utf-8'))
+ return toolkit.redirect_to(auth_url)#.encode('utf-8'))
def get_token(self):
oauth = OAuth2Session(self.client_id, redirect_uri=self.redirect_uri, scope=self.scope)
@@ -111,41 +108,40 @@ def get_token(self):
if self.legacy_idm:
# This is only required for Keyrock v6 and v5
headers['Authorization'] = 'Basic %s' % base64.urlsafe_b64encode(
- '%s:%s' % (self.client_id, self.client_secret)
+ (f'{self.client_id}:{self.client_secret}').encode()
)
try:
+ log.debug(f'authorization_response: {toolkit.request.url}')
token = oauth.fetch_token(self.token_endpoint,
- headers=headers,
+ client_id=self.client_id,
client_secret=self.client_secret,
- authorization_response=toolkit.request.url,
- verify=self.verify_https)
+ authorization_response=toolkit.request.url.replace('http:', 'https:', 1))
except requests.exceptions.SSLError as e:
# TODO search a better way to detect invalid certificates
if "verify failed" in six.text_type(e):
raise InsecureTransportError()
else:
raise
-
return token
def identify(self, token):
-
if self.jwt_enable:
-
+ log.debug('jwt_enabled')
access_token = bytes(token['access_token'])
user_data = jwt.decode(access_token, verify=False)
user = self.user_json(user_data)
- else:
+ else:
try:
if self.legacy_idm:
profile_response = requests.get(self.profile_api_url + '?access_token=%s' % token['access_token'], verify=self.verify_https)
else:
oauth = OAuth2Session(self.client_id, token=token)
- profile_response = oauth.get(self.profile_api_url, verify=self.verify_https)
+ profile_response = oauth.get(self.profile_api_url)
except requests.exceptions.SSLError as e:
+ log.debug('exception identify oauth2')
# TODO search a better way to detect invalid certificates
if "verify failed" in six.text_type(e):
raise InsecureTransportError()
@@ -162,6 +158,7 @@ def identify(self, token):
else:
user_data = profile_response.json()
user = self.user_json(user_data)
+ log.debug(f'user: {user}')
# Save the user in the database
model.Session.add(user)
@@ -171,6 +168,7 @@ def identify(self, token):
return user.name
def user_json(self, user_data):
+ log.debug(f'user_data: {user_data}')
email = user_data[self.profile_api_mail_field]
user_name = user_data[self.profile_api_user_field]
@@ -214,15 +212,24 @@ def remember(self, user_name):
rememberer = self._get_rememberer(environ)
identity = {'repoze.who.userid': user_name}
headers = rememberer.remember(environ, identity)
+ response = jsonify()
for header, value in headers:
- toolkit.response.headers.add(header, value)
+ response.headers[header] = value
+ return response
- def redirect_from_callback(self):
+ def redirect_from_callback(self, resp_remember):
'''Redirect to the callback URL after a successful authentication.'''
state = toolkit.request.params.get('state')
came_from = get_came_from(state)
- toolkit.response.status = 302
- toolkit.response.location = came_from
+
+ response = jsonify()
+ response.status_code = 302
+ for header, value in resp_remember.headers:
+ response.headers[header] = value
+ response.headers['location'] = came_from
+ response.autocorrect_location_header = False
+ return response
+
def get_stored_token(self, user_name):
user_token = db.UserToken.by_user_name(user_name=user_name)
@@ -235,8 +242,10 @@ def get_stored_token(self, user_name):
}
def update_token(self, user_name, token):
-
- user_token = db.UserToken.by_user_name(user_name=user_name)
+ try:
+ user_token = db.UserToken.by_user_name(user_name=user_name)
+ except AttributeError as e:
+ user_token = None
# Create the user if it does not exist
if not user_token:
user_token = db.UserToken()
diff --git a/ckanext/oauth2/plugin.py b/ckanext/oauth2/plugin.py
index 94203a3..1c41df6 100644
--- a/ckanext/oauth2/plugin.py
+++ b/ckanext/oauth2/plugin.py
@@ -18,17 +18,18 @@
# You should have received a copy of the GNU Affero General Public License
# along with OAuth2 CKAN Extension. If not, see .
-from __future__ import unicode_literals
-
import logging
-import oauth2
+from .oauth2 import *
import os
from functools import partial
from ckan import plugins
from ckan.common import g
from ckan.plugins import toolkit
-from urlparse import urlparse
+import ckanext.oauth2.db as db
+import urllib.parse
+from ckanext.oauth2.views import get_blueprints
+from ckanext.oauth2.cli import get_commands
log = logging.getLogger(__name__)
@@ -62,66 +63,33 @@ def request_reset(context, data_dict):
return _no_permissions(context, msg)
-def _get_previous_page(default_page):
- if 'came_from' not in toolkit.request.params:
- came_from_url = toolkit.request.headers.get('Referer', default_page)
- else:
- came_from_url = toolkit.request.params.get('came_from', default_page)
-
- came_from_url_parsed = urlparse(came_from_url)
-
- # Avoid redirecting users to external hosts
- if came_from_url_parsed.netloc != '' and came_from_url_parsed.netloc != toolkit.request.host:
- came_from_url = default_page
-
- # When a user is being logged and REFERER == HOME or LOGOUT_PAGE
- # he/she must be redirected to the dashboard
- pages = ['/', '/user/logged_out_redirect']
- if came_from_url_parsed.path in pages:
- came_from_url = default_page
-
- return came_from_url
-
-
class OAuth2Plugin(plugins.SingletonPlugin):
-
plugins.implements(plugins.IAuthenticator, inherit=True)
plugins.implements(plugins.IAuthFunctions, inherit=True)
- plugins.implements(plugins.IRoutes, inherit=True)
plugins.implements(plugins.IConfigurer)
+ plugins.implements(plugins.IBlueprint)
+ plugins.implements(plugins.IClick)
+ plugins.implements(plugins.IRoutes)
- def __init__(self, name=None):
- '''Store the OAuth 2 client configuration'''
- log.debug('Init OAuth2 extension')
+ # IBlueprint
- self.oauth2helper = oauth2.OAuth2Helper()
+ def get_blueprint(self):
+ return get_blueprints()
- def before_map(self, m):
- log.debug('Setting up the redirections to the OAuth2 service')
+ # IClick
- m.connect('/user/login',
- controller='ckanext.oauth2.controller:OAuth2Controller',
- action='login')
+ def get_commands(self):
+ return get_commands()
- # We need to handle petitions received to the Callback URL
- # since some error can arise and we need to process them
- m.connect('/oauth2/callback',
- controller='ckanext.oauth2.controller:OAuth2Controller',
- action='callback')
- # Redirect the user to the OAuth service register page
- if self.register_url:
- m.redirect('/user/register', self.register_url)
-
- # Redirect the user to the OAuth service reset page
- if self.reset_url:
- m.redirect('/user/reset', self.reset_url)
+ def __init__(self, name=None):
+ '''Store the OAuth 2 client configuration'''
+ log.debug('Init OAuth2 extension')
- # Redirect the user to the OAuth service reset page
- if self.edit_url:
- m.redirect('/user/edit/{user}', self.edit_url)
+ db.init_db(model)
+ log.debug(f'Creating UserToken...')
+ self.oauth2helper = OAuth2Helper()
- return m
def identify(self):
log.debug('identify')
@@ -129,12 +97,13 @@ def identify(self):
def _refresh_and_save_token(user_name):
new_token = self.oauth2helper.refresh_token(user_name)
if new_token:
- toolkit.c.usertoken = new_token
+ toolkit.g.usertoken = new_token
environ = toolkit.request.environ
apikey = toolkit.request.headers.get(self.authorization_header, '')
user_name = None
+
if self.authorization_header == "authorization":
if apikey.startswith('Bearer '):
apikey = apikey[7:].strip()
@@ -146,7 +115,10 @@ def _refresh_and_save_token(user_name):
try:
token = {'access_token': apikey}
user_name = self.oauth2helper.identify(token)
- except Exception:
+ log.debug(f'user_name1: {user_name}')
+ except Exception as e:
+ log.debug(f'Auth error:')
+ log.debug(e)
pass
# If the authentication via API fails, we can still log in the user using session.
@@ -157,11 +129,12 @@ def _refresh_and_save_token(user_name):
# If we have been able to log in the user (via API or Session)
if user_name:
g.user = user_name
- toolkit.c.user = user_name
- toolkit.c.usertoken = self.oauth2helper.get_stored_token(user_name)
- toolkit.c.usertoken_refresh = partial(_refresh_and_save_token, user_name)
+ toolkit.g.user = user_name
+ toolkit.g.usertoken = self.oauth2helper.get_stored_token(user_name)
+ toolkit.g.usertoken_refresh = partial(_refresh_and_save_token, user_name)
else:
g.user = None
+ toolkit.g.user = None
log.warn('The user is not currently logged...')
def get_auth_functions(self):
@@ -175,6 +148,7 @@ def get_auth_functions(self):
def update_config(self, config):
# Update our configuration
+ log.debug('update config...')
self.register_url = os.environ.get("CKAN_OAUTH2_REGISTER_URL", config.get('ckan.oauth2.register_url', None))
self.reset_url = os.environ.get("CKAN_OAUTH2_RESET_URL", config.get('ckan.oauth2.reset_url', None))
self.edit_url = os.environ.get("CKAN_OAUTH2_EDIT_URL", config.get('ckan.oauth2.edit_url', None))
@@ -183,3 +157,31 @@ def update_config(self, config):
# Add this plugin's templates dir to CKAN's extra_template_paths, so
# that CKAN will use this plugin's custom templates.
plugins.toolkit.add_template_directory(config, 'templates')
+
+
+ def before_map(self, m):
+ log.debug('Setting up the redirections to the OAuth2 service')
+
+ m.connect('/user/login',
+ controller='ckanext.oauth2.controller:OAuth2Controller',
+ action='login')
+
+ # We need to handle petitions received to the Callback URL
+ # since some error can arise and we need to process them
+ m.connect('/oauth2/callback',
+ controller='ckanext.oauth2.controller:OAuth2Controller',
+ action='callback')
+
+ # Redirect the user to the OAuth service register page
+ if self.register_url:
+ m.redirect('/user/register', self.register_url)
+
+ # Redirect the user to the OAuth service reset page
+ if self.reset_url:
+ m.redirect('/user/reset', self.reset_url)
+
+ # Redirect the user to the OAuth service reset page
+ if self.edit_url:
+ m.redirect('/user/edit/{user}', self.edit_url)
+
+ return m
\ No newline at end of file
diff --git a/ckanext/oauth2/tests/test_db.py b/ckanext/oauth2/tests/test_db.py
index b7b1ad8..01889c5 100644
--- a/ckanext/oauth2/tests/test_db.py
+++ b/ckanext/oauth2/tests/test_db.py
@@ -27,7 +27,7 @@ class DBTest(unittest.TestCase):
def setUp(self):
# Restart databse initial status
- db.UserToken = None
+ db.UserToken = None
# Create mocks
self._sa = db.sa
diff --git a/ckanext/oauth2/tests/test_plugin.py b/ckanext/oauth2/tests/test_plugin.py
index 1ce8932..5d6646a 100644
--- a/ckanext/oauth2/tests/test_plugin.py
+++ b/ckanext/oauth2/tests/test_plugin.py
@@ -174,9 +174,9 @@ def authenticate_side_effect(identity):
plugin.toolkit.request.headers = headers
# The identify function must set the user id in this variable
- plugin.toolkit.c.user = None
- plugin.toolkit.c.usertoken = None
- plugin.toolkit.c.usertoken_refresh = None
+ plugin.toolkit.g.user = None
+ plugin.toolkit.g.usertoken = None
+ plugin.toolkit.g.usertoken_refresh = None
# Call the function
self._plugin.identify()
@@ -191,15 +191,15 @@ def authenticate_side_effect(identity):
self.assertEquals(0, self._plugin.oauth2helper.identify.call_count)
self.assertEquals(expected_user, g_mock.user)
- self.assertEquals(expected_user, plugin.toolkit.c.user)
+ self.assertEquals(expected_user, plugin.toolkit.g.user)
if expected_user is None:
- self.assertIsNone(plugin.toolkit.c.usertoken)
- self.assertIsNone(plugin.toolkit.c.usertoken_refresh)
+ self.assertIsNone(plugin.toolkit.g.usertoken)
+ self.assertIsNone(plugin.toolkit.g.usertoken_refresh)
else:
- self.assertEquals(usertoken, plugin.toolkit.c.usertoken)
+ self.assertEquals(usertoken, plugin.toolkit.g.usertoken)
# method 'usertoken_refresh' should relay on the one provided by the repoze.who module
- plugin.toolkit.c.usertoken_refresh()
+ plugin.toolkit.g.usertoken_refresh()
self._plugin.oauth2helper.refresh_token.assert_called_once_with(expected_user)
- self.assertEquals(newtoken, plugin.toolkit.c.usertoken)
+ self.assertEquals(newtoken, plugin.toolkit.g.usertoken)
diff --git a/ckanext/oauth2/views.py b/ckanext/oauth2/views.py
new file mode 100644
index 0000000..7b15cdd
--- /dev/null
+++ b/ckanext/oauth2/views.py
@@ -0,0 +1,86 @@
+import logging
+from flask import Blueprint, jsonify, make_response
+import logging
+from ckanext.oauth2 import constants
+from ckan.common import session
+import ckan.lib.helpers as helpers
+import ckan.plugins.toolkit as toolkit
+import urllib.parse
+from ckanext.oauth2.oauth2 import OAuth2Helper
+
+log = logging.getLogger(__name__)
+
+oauth2 = Blueprint("oauth2", __name__)
+
+oauth2helper = OAuth2Helper()
+
+def _get_previous_page(default_page):
+ if 'came_from' not in toolkit.request.params:
+ came_from_url = toolkit.request.headers.get('Referer', default_page)
+ else:
+ came_from_url = toolkit.request.params.get('came_from', default_page)
+
+ came_from_url_parsed = urllib.parse.urlparse(came_from_url)
+
+ # Avoid redirecting users to external hosts
+ if came_from_url_parsed.netloc != '' and came_from_url_parsed.netloc != toolkit.request.host:
+ came_from_url = default_page
+
+ # When a user is being logged and REFERER == HOME or LOGOUT_PAGE
+ # he/she must be redirected to the dashboard
+ pages = ['/', '/user/logged_out_redirect']
+ if came_from_url_parsed.path in pages:
+ came_from_url = default_page
+
+ return came_from_url
+
+@oauth2.route('/user/login')
+def login():
+ log.debug('login')
+
+ # Log in attemps are fired when the user is not logged in and they click
+ # on the log in button
+
+ # Get the page where the user was when the loggin attemp was fired
+ # When the user is not logged in, he/she should be redirected to the dashboard when
+ # the system cannot get the previous page
+ came_from_url = _get_previous_page(constants.INITIAL_PAGE)
+ return oauth2helper.challenge(came_from_url)
+
+@oauth2.route('/oauth2/callback')
+def callback():
+
+ try:
+ token = oauth2helper.get_token()
+
+ user_name = oauth2helper.identify(token)
+ response = oauth2helper.remember(user_name)
+ log.debug(f'usr:{user_name}')
+
+ oauth2helper.update_token(user_name, token)
+ response = oauth2helper.redirect_from_callback(response)
+ except Exception as e:
+
+ session.save()
+
+ # If the callback is called with an error, we must show the message
+ error_description = toolkit.request.GET.get('error_description')
+ if not error_description:
+ if e.message:
+ error_description = e.message
+ elif hasattr(e, 'description') and e.description:
+ error_description = e.description
+ elif hasattr(e, 'error') and e.error:
+ error_description = e.error
+ else:
+ error_description = type(e).__name__
+ response = jsonify()
+ response.status_code = 302
+ redirect_url = oauth2.get_came_from(toolkit.request.params.get('state'))
+ redirect_url = '/' if redirect_url == constants.INITIAL_PAGE else redirect_url
+ response.location = redirect_url
+ helpers.flash_error(error_description)
+ return response
+
+def get_blueprints():
+ return [oauth2]
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..ba45b54
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,2 @@
+requests-oauthlib==0.8.0
+pyjwt==1.7.1
\ No newline at end of file
diff --git a/setup.py b/setup.py
index 48c927a..e409087 100644
--- a/setup.py
+++ b/setup.py
@@ -86,9 +86,6 @@ def rst(filename):
entry_points={
'ckan.plugins': [
'oauth2 = ckanext.oauth2.plugin:OAuth2Plugin',
- ],
- 'nose.plugins': [
- 'pylons = pylons.test:PylonsPlugin'
]
},
classifiers=[