From e1e2d105735c71e96791216b89862675022fd13c Mon Sep 17 00:00:00 2001 From: Rebecka Gulliksson Date: Thu, 11 Feb 2016 10:41:07 +0100 Subject: [PATCH] Initial commit. --- .bumpversion.cfg | 7 +++ .gitignore | 5 +++ README.md | 30 +++++++++++++ example/app.py | 22 +++++++++ setup.py | 17 +++++++ src/flask_pyoidc.py | 106 ++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 187 insertions(+) create mode 100644 .bumpversion.cfg create mode 100644 .gitignore create mode 100644 README.md create mode 100644 example/app.py create mode 100644 setup.py create mode 100644 src/flask_pyoidc.py diff --git a/.bumpversion.cfg b/.bumpversion.cfg new file mode 100644 index 0000000..e4ec7dd --- /dev/null +++ b/.bumpversion.cfg @@ -0,0 +1,7 @@ +[bumpversion] +current_version = 0.0.1 +commit = True +tag = True + +[bumpversion:file:setup.py] + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b98cec0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +*.pyc +*.egg-info +build/ +dist/ +.idea/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..e23c003 --- /dev/null +++ b/README.md @@ -0,0 +1,30 @@ +# Flask-pyoidc + +![PyPI](https://img.shields.io/pypi/v/flask-pyoidc.svg) + +This repository contains an example of how to use the [pyoidc](https://github.com/rohe/pyoidc) +library to provide simple OpenID Connect authentication (using the ["Code Flow"](http://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth). + +## Usage + +The extension support both static and dynamic provider configuration discovery as well as static +and dynamic client registration. The different modes of provider configuration can be combined in +any way with the different client registration modes. + +* Static provider configuration: `OIDCAuthentication(provider_configuration_info=provider_config)`, + where `provider_config` is a dictionary containing the [provider metadata](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata). +* Dynamic provider configuration: `OIDCAuthentication(issuer=issuer_url)`, where `issuer_url` + is the issuer URL of the provider. +* Static client registration: `OIDCAuthentication(client_registration_info=client_info)`, where + `client_info` is all the [registered metadata](https://openid.net/specs/openid-connect-registration-1_0.html#RegistrationResponse) + about the client. The `redirect_uris` registered with the provider MUST include + `/redirect_uri`, where `` is the URL for the Flask application. + + + +The application using this extension MUST set the following [builtin configuration values of Flask](http://flask.pocoo.org/docs/0.10/config/#builtin-configuration-values): + +* `SERVER_NAME` (MUST be the same as `` if using static client registration +* `SECRET_KEY` (this extension relies on Flask session, which requires `SECRET_KEY`) + +Have a look at the example Flask app in [app.py](example/app.py) for an idea of how to use it. \ No newline at end of file diff --git a/example/app.py b/example/app.py new file mode 100644 index 0000000..74201d5 --- /dev/null +++ b/example/app.py @@ -0,0 +1,22 @@ +import flask +from flask import Flask, jsonify + +from flask_pyoidc import OIDCAuthentication + +PORT = 5000 +app = Flask(__name__) + +app.config.update({'SERVER_NAME': 'localhost:{}'.format(PORT), + 'SECRET_KEY': 'dev_key'}) +auth = OIDCAuthentication(app, issuer="https://localhost:50009") + + +@app.route('/') +@auth.oidc_auth +def index(): + return jsonify(id_token=flask.g.id_token.to_dict(), access_token=flask.g.access_token, + userinfo=flask.g.userinfo.to_dict()) + + +if __name__ == '__main__': + app.run(port=PORT) diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..c264a51 --- /dev/null +++ b/setup.py @@ -0,0 +1,17 @@ +from setuptools import setup, find_packages + +setup( + name='Flask-pyoidc', + version='0.0.1', + packages=find_packages('src'), + package_dir={'': 'src'}, + url='https://github.com/its-dirg/flask-pyoidc', + license='Apache 2.0', + author='Rebecka Gulliksson', + author_email='rebecka.gulliksson@umu.se', + description='Flask extension for OpenID Connect authentication.', + install_requires=[ + 'oic', + 'Flask' + ] +) diff --git a/src/flask_pyoidc.py b/src/flask_pyoidc.py new file mode 100644 index 0000000..ce62fb2 --- /dev/null +++ b/src/flask_pyoidc.py @@ -0,0 +1,106 @@ +import functools + +import flask +from flask.helpers import url_for +from oic.oauth2 import rndstr +from oic.oic import Client +from oic.oic.message import ProviderConfigurationResponse, RegistrationRequest, \ + AuthorizationResponse +from oic.utils.authn.client import CLIENT_AUTHN_METHOD +from werkzeug.utils import redirect + + +class OIDCAuthentication(object): + def __init__(self, flask_app, client_registration_info=None, issuer=None, + provider_configuration_info=None): + self.app = flask_app + + self.client = Client(client_authn_method=CLIENT_AUTHN_METHOD) + if not issuer and not provider_configuration_info: + raise ValueError( + 'Either \'issuer\' (for dynamic discovery) or \'provider_configuration_info\' (for static configuration must be specified.') + if issuer and not provider_configuration_info: + self.client.provider_config(issuer) + else: + self.client.handle_provider_config( + ProviderConfigurationResponse(**provider_configuration_info), + provider_configuration_info['issuer']) + + self.client_registration_info = client_registration_info or {} + if client_registration_info and 'client_id' in client_registration_info: + # static client info provided + self.client.store_registration_info(RegistrationRequest(**client_registration_info)) + else: + # do dynamic registration + self.app.add_url_rule('/redirect_uri', 'redirect_uri', + self._handle_authentication_response) + with self.app.app_context(): + self.client_registration_info['redirect_uris'] = url_for('redirect_uri') + self.client.register(self.client.provider_info['registration_endpoint'], + **self.client_registration_info) + + self.callback = None + + def _authenticate(self): + if flask.g.get('userinfo', None): + return self.callback() + + flask.session['state'] = rndstr() + flask.session['nonce'] = rndstr() + args = { + 'client_id': self.client.client_id, + 'response_type': 'code', + 'scope': ['openid'], + 'redirect_uri': self.client.registration_response['redirect_uris'][0], + 'state': flask.session['state'], + 'nonce': flask.session['nonce'], + } + + auth_req = self.client.construct_AuthorizationRequest(request_args=args) + login_url = auth_req.request(self.client.authorization_endpoint) + return redirect(login_url) + + def _handle_authentication_response(self): + # parse authentication response + query_string = flask.request.query_string.decode('utf-8') + authn_resp = self.client.parse_response(AuthorizationResponse, info=query_string, + sformat='urlencoded') + + if authn_resp['state'] != flask.session['state']: + raise ValueError('The \'state\' parameter does not match.') + + # do token request + args = { + 'code': authn_resp['code'], + 'redirect_uri': self.client.registration_response['redirect_uris'][0], + 'client_id': self.client.client_id, + 'client_secret': self.client.client_secret + } + token_resp = self.client.do_access_token_request(scope='openid', state=authn_resp['state'], + request_args=args, + authn_method='client_secret_basic') + id_token = token_resp['id_token'] + if id_token['nonce'] != flask.session['nonce']: + raise ValueError('The \'nonce\' parameter does not match.') + access_token = token_resp['access_token'] + + # do userinfo request + userinfo = self.client.do_user_info_request(state=authn_resp['state']) + if userinfo['sub'] != id_token['sub']: + raise ValueError('The \'sub\' of userinfo does not match \'sub\' of ID Token.') + + # store the current user + flask.g.id_token = id_token + flask.g.access_token = access_token + flask.g.userinfo = userinfo + + return self.callback() + + def oidc_auth(self, f): + self.callback = f + + @functools.wraps(f) + def wrapper(): + return self._authenticate() + + return wrapper