From 2cc3eb256bbfc08411bd69f5c5f4887792b542e8 Mon Sep 17 00:00:00 2001 From: Matt Jia Date: Apr 06 2017 23:32:22 +0000 Subject: support OIDC authentication --- diff --git a/conf/client_secrets.json b/conf/client_secrets.json new file mode 100644 index 0000000..91985b0 --- /dev/null +++ b/conf/client_secrets.json @@ -0,0 +1,11 @@ +{ + "web": { + "redirect_uris": ["http://localhost:5005/"], + "token_uri": "https://iddev.fedorainfracloud.org/openidc/Token", + "auth_uri": "https://iddev.fedorainfracloud.org/openidc/Authorization", + "client_id": "D-e69a1ac7-30fa-4d18-9001-7468c4f34c3c", + "client_secret": "qgz8Bzjg6nO7JWCXoB0o8L49KfI5atLF", + "userinfo_uri": "https://iddev.fedorainfracloud.org/openidc/UserInfo", + "token_introspection_uri": "https://iddev.fedorainfracloud.org/openidc/TokenInfo" + } +} diff --git a/requirements.txt b/requirements.txt index a201a83..d5c9310 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,9 @@ Flask-RESTful Flask-SQLAlchemy SQLAlchemy kerberos >= 1.1.1 +flask-oidc +# packages for the unit tests pytest >= 2.4.2 mock diff --git a/tests/client_secrets.json b/tests/client_secrets.json new file mode 100644 index 0000000..9e72bf7 --- /dev/null +++ b/tests/client_secrets.json @@ -0,0 +1,11 @@ +{ + "web": { + "redirect_uris": ["http://localhost:5005/"], + "token_uri": "https://iddev.fedorainfracloud.org/openidc/Token", + "auth_uri": "https://iddev.fedorainfracloud.org/openidc/Authorization", + "client_id": "randomid", + "client_secret": "nosecret", + "userinfo_uri": "https://iddev.fedorainfracloud.org/openidc/UserInfo", + "token_introspection_uri": "https://iddev.fedorainfracloud.org/openidc/TokenInfo" + } +} diff --git a/tests/conftest.py b/tests/conftest.py index c08336e..59a2529 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -11,7 +11,6 @@ # import pytest - from waiverdb.app import create_app, init_db @@ -59,3 +58,8 @@ def client(app): """ with app.test_client() as client: yield client + + +@pytest.fixture() +def enable_kerberos(app, monkeypatch): + monkeypatch.setitem(app.config, 'AUTH_METHOD', 'Kerberos') diff --git a/tests/test_app.py b/tests/test_app.py index 3055c3a..aba4648 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -23,10 +23,12 @@ from flask_sqlalchemy import SignallingSession class NoZmqConfig(config.Config): ZEROMQ_PUBLISH = False + AUTH_METHOD = None class ZmqConfig(config.Config): ZEROMQ_PUBLISH = True + AUTH_METHOD = None @mock.patch('waiverdb.app.event.listen') diff --git a/tests/test_auth.py b/tests/test_auth.py index 7f534af..cbc5963 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -15,13 +15,20 @@ import mock import json from werkzeug.exceptions import Unauthorized import waiverdb.auth +import flask_oidc +@pytest.mark.usefixtures('enable_kerberos') class TestKerberosAuthentication(object): def test_keytab_file_is_not_set_should_raise_error(self): with pytest.raises(Unauthorized): request = mock.MagicMock() + headers = {'Authorization': "babablaba"} + request.headers.return_value = mock.MagicMock(spec_set=dict) + request.headers.__getitem__.side_effect = headers.__getitem__ + request.headers.__setitem__.side_effect = headers.__setitem__ + request.headers.__contains__.side_effect = headers.__contains__ waiverdb.auth.get_user(request) def test_unauthorized(self, client, monkeypatch): @@ -52,3 +59,43 @@ class TestKerberosAuthentication(object): assert r.headers.get('WWW-Authenticate') == 'negotiate STOKEN' res_data = json.loads(r.data.decode('utf-8')) assert res_data['username'] == 'foo' + + +class TestOIDCAuthentication(object): + + def test_get_user_without_token(self, session): + with pytest.raises(Unauthorized) as excinfo: + request = mock.MagicMock() + waiverdb.auth.get_user(request) + assert "No 'Authorization' header found" in str(excinfo.value) + + @mock.patch.object(flask_oidc.OpenIDConnect, '_get_token_info') + def test_get_user_with_invalid_token(self, mocked_get_token, session): + # http://vsbattles.wikia.com/wiki/Son_Goku + name = 'Son Goku' + mocked_get_token.return_value = {'active': False, 'username': name, + 'scope': 'openid waiverdb_scope'} + headers = {'Authorization': 'Bearer invalid'} + request = mock.MagicMock() + request.headers.return_value = mock.MagicMock(spec_set=dict) + request.headers.__getitem__.side_effect = headers.__getitem__ + request.headers.__setitem__.side_effect = headers.__setitem__ + request.headers.__contains__.side_effect = headers.__contains__ + with pytest.raises(Unauthorized) as excinfo: + waiverdb.auth.get_user(request) + assert 'Token required but invalid' in str(excinfo.value) + + @mock.patch.object(flask_oidc.OpenIDConnect, '_get_token_info') + def test_get_user_good(self, mocked_get_token, session): + # http://vsbattles.wikia.com/wiki/Son_Goku + name = 'Son Goku' + mocked_get_token.return_value = {'active': True, 'username': name, + 'scope': 'openid waiverdb_scope'} + headers = {'Authorization': 'Bearer foobar'} + request = mock.MagicMock() + request.headers.return_value = mock.MagicMock(spec_set=dict) + request.headers.__getitem__.side_effect = headers.__getitem__ + request.headers.__setitem__.side_effect = headers.__setitem__ + request.headers.__contains__.side_effect = headers.__contains__ + user, header = waiverdb.auth.get_user(request) + assert user == name diff --git a/waiverdb/app.py b/waiverdb/app.py index 3e31b45..d740b84 100644 --- a/waiverdb/app.py +++ b/waiverdb/app.py @@ -18,6 +18,7 @@ from waiverdb.events import fedmsg_new_waiver from waiverdb.logger import init_logging from waiverdb.api_v1 import api_v1 from waiverdb.models import db +from flask_oidc import OpenIDConnect def load_default_config(app): @@ -48,6 +49,8 @@ def create_app(config_obj=None): raise Warning("You need to change the app.secret_key value for production") if app.config['SHOW_DB_URI']: app.logger.debug('using DBURI: %s' % app.config['SQLALCHEMY_DATABASE_URI']) + if app.config['AUTH_METHOD'] == 'OIDC': + app.oidc = OpenIDConnect(app) # initialize db db.init_app(app) # initialize logging diff --git a/waiverdb/auth.py b/waiverdb/auth.py index 8f45935..cb796fa 100644 --- a/waiverdb/auth.py +++ b/waiverdb/auth.py @@ -9,9 +9,10 @@ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. + import os import kerberos -from flask import current_app, Response +from flask import current_app, Response, g # Starting with Flask 0.9, the _app_ctx_stack is the correct one, # before that we need to use the _request_ctx_stack. try: @@ -93,12 +94,28 @@ class KerberosAuthenticate(object): def get_user(request): user = None headers = dict() - if 'KRB5_KTNAME' in os.environ: - header = request.headers.get("Authorization") - if not header: + if current_app.config['AUTH_METHOD'] == 'OIDC': + if 'Authorization' not in request.headers: + raise Unauthorized("No 'Authorization' header found.") + token = request.headers.get("Authorization").strip() + prefix = 'Bearer ' + if not token.startswith(prefix): + raise Unauthorized('Authorization headers must start with %r' % prefix) + token = token[len(prefix):].strip() + required_scopes = [ + 'openid', + current_app.config['OIDC_REQUIRED_SCOPE'], + ] + validity = current_app.oidc.validate_token(token, required_scopes) + if validity is not True: + raise Unauthorized(validity) + user = g.oidc_token_info['username'] + elif current_app.config['AUTH_METHOD'] == 'Kerberos': + if 'Authorization' not in request.headers: response = Response('Unauthorized', 401, {'WWW-Authenticate': 'Negotiate'}) raise Unauthorized(response=response) - token = ''.join(header.split()[1:]) + header = request.headers.get("Authorization") + token = ''.join(header.strip().split()[1:]) user, kerberos_token = KerberosAuthenticate().process_request(token) # remove realm user = user.split("@")[0] diff --git a/waiverdb/config.py b/waiverdb/config.py index dd03a3c..421d6cf 100644 --- a/waiverdb/config.py +++ b/waiverdb/config.py @@ -9,6 +9,8 @@ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. +import os + class Config(object): """ @@ -29,6 +31,7 @@ class Config(object): # need to explicitly turn this off # https://github.com/flask-restful/flask-restful/issues/449 ERROR_404_HELP = False + AUTH_METHOD = 'OIDC' # Specify OIDC or Kerberos for authentication # Change it if the Kerberos service is not running on which the waiverdb is run. KERBEROS_HTTP_HOST = None ZEROMQ_PUBLISH = True @@ -44,9 +47,22 @@ class DevelopmentConfig(Config): TRAP_BAD_REQUEST_ERRORS = True SQLALCHEMY_DATABASE_URI = 'sqlite:////var/tmp/waiverdb_db.sqlite' SHOW_DB_URI = True + # The location of the client_secrets.json file used for API authentication + OIDC_CLIENT_SECRETS = os.path.join( + os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'conf', + 'client_secrets.json' + ) + OIDC_REQUIRED_SCOPE = 'https://waiverdb.fedoraproject.org/oidc/create-waiver' + OIDC_RESOURCE_SERVER_ONLY = True class TestingConfig(Config): SQLALCHEMY_TRACK_MODIFICATIONS = True TRAP_BAD_REQUEST_ERRORS = True TESTING = True + OIDC_CLIENT_SECRETS = os.path.join( + os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'tests', + 'client_secrets.json' + ) + OIDC_REQUIRED_SCOPE = 'waiverdb_scope' + OIDC_RESOURCE_SERVER_ONLY = True