Skip to content

Commit 513105c

Browse files
author
Rebecka Gulliksson
committed
Initial commit.
0 parents  commit 513105c

6 files changed

Lines changed: 185 additions & 0 deletions

File tree

.bumpversion.cfg

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
[bumpversion]
2+
current_version = 0.0.1
3+
commit = True
4+
tag = True
5+
6+
[bumpversion:file:setup.py]
7+

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
*.pyc
2+
*.egg-info
3+
build/
4+
dist/
5+
.idea/

README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Flask-pyoidc
2+
3+
This repository contains an example of how to use the [pyoidc](https://github.com/rohe/pyoidc)
4+
library to provide simple OpenID Connect authentication (using the ["Code Flow"](http://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth).
5+
6+
## Usage
7+
8+
The extension support both static and dynamic provider configuration discovery as well as static
9+
and dynamic client registration. The different modes of provider configuration can be combined in
10+
any way with the different client registration modes.
11+
12+
* Static provider configuration: `OIDCAuthentication(provider_configuration_info=provider_config)`,
13+
where `provider_config` is a dictionary containing the [provider metadata](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata).
14+
* Dynamic provider configuration: `OIDCAuthentication(issuer=issuer_url)`, where `issuer_url`
15+
is the issuer URL of the provider.
16+
* Static client registration: `OIDCAuthentication(client_registration_info=client_info)`, where
17+
`client_info` is all the [registered metadata](https://openid.net/specs/openid-connect-registration-1_0.html#RegistrationResponse)
18+
about the client. The `redirect_uris` registered with the provider MUST include
19+
`<flask_url>/redirect_uri`, where `<flask_url>` is the URL for the Flask application.
20+
21+
22+
23+
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):
24+
25+
* `SERVER_NAME` (MUST be the same as `<flask_url>` if using static client registration
26+
* `SECRET_KEY` (this extension relies on Flask session, which requires `SECRET_KEY`)
27+
28+
Have a look at the example Flask app in [app.py](example/app.py) for an idea of how to use it.

example/app.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import flask
2+
from flask import Flask, jsonify
3+
4+
from flask_pyoidc import OIDCAuthentication
5+
6+
PORT = 5000
7+
app = Flask(__name__)
8+
9+
app.config.update({'SERVER_NAME': 'localhost:{}'.format(PORT),
10+
'SECRET_KEY': 'dev_key'})
11+
auth = OIDCAuthentication(app, issuer="https://localhost:50009")
12+
13+
14+
@app.route('/')
15+
@auth.oidc_auth
16+
def index():
17+
return jsonify(id_token=flask.g.id_token.to_dict(), access_token=flask.g.access_token,
18+
userinfo=flask.g.userinfo.to_dict())
19+
20+
21+
if __name__ == '__main__':
22+
app.run(port=PORT)

setup.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from setuptools import setup, find_packages
2+
3+
setup(
4+
name='Flask-pyoidc',
5+
version='0.0.1',
6+
packages=find_packages('src'),
7+
package_dir={'': 'src'},
8+
url='https://github.com/its-dirg/flask-pyoidc',
9+
license='Apache 2.0',
10+
author='Rebecka Gulliksson',
11+
author_email='rebecka.gulliksson@umu.se',
12+
description='Flask extension for OpenID Connect authentication.',
13+
install_requires=[
14+
'oic',
15+
'Flask'
16+
]
17+
)

src/flask_pyoidc.py

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import functools
2+
3+
import flask
4+
from flask.helpers import url_for
5+
from oic.oauth2 import rndstr
6+
from oic.oic import Client
7+
from oic.oic.message import ProviderConfigurationResponse, RegistrationRequest, \
8+
AuthorizationResponse
9+
from oic.utils.authn.client import CLIENT_AUTHN_METHOD
10+
from werkzeug.utils import redirect
11+
12+
13+
class OIDCAuthentication(object):
14+
def __init__(self, flask_app, client_registration_info=None, issuer=None,
15+
provider_configuration_info=None):
16+
self.app = flask_app
17+
18+
self.client = Client(client_authn_method=CLIENT_AUTHN_METHOD)
19+
if not issuer and not provider_configuration_info:
20+
raise ValueError(
21+
'Either \'issuer\' (for dynamic discovery) or \'provider_configuration_info\' (for static configuration must be specified.')
22+
if issuer and not provider_configuration_info:
23+
self.client.provider_config(issuer)
24+
else:
25+
self.client.handle_provider_config(
26+
ProviderConfigurationResponse(**provider_configuration_info),
27+
provider_configuration_info['issuer'])
28+
29+
self.client_registration_info = client_registration_info or {}
30+
if client_registration_info and 'client_id' in client_registration_info:
31+
# static client info provided
32+
self.client.store_registration_info(RegistrationRequest(**client_registration_info))
33+
else:
34+
# do dynamic registration
35+
self.app.add_url_rule('/redirect_uri', 'redirect_uri',
36+
self._handle_authentication_response)
37+
with self.app.app_context():
38+
self.client_registration_info['redirect_uris'] = url_for('redirect_uri')
39+
self.client.register(self.client.provider_info['registration_endpoint'],
40+
**self.client_registration_info)
41+
42+
self.callback = None
43+
44+
def _authenticate(self):
45+
if flask.g.get('userinfo', None):
46+
return self.callback()
47+
48+
flask.session['state'] = rndstr()
49+
flask.session['nonce'] = rndstr()
50+
args = {
51+
'client_id': self.client.client_id,
52+
'response_type': 'code',
53+
'scope': ['openid'],
54+
'redirect_uri': self.client.registration_response['redirect_uris'][0],
55+
'state': flask.session['state'],
56+
'nonce': flask.session['nonce'],
57+
}
58+
59+
auth_req = self.client.construct_AuthorizationRequest(request_args=args)
60+
login_url = auth_req.request(self.client.authorization_endpoint)
61+
return redirect(login_url)
62+
63+
def _handle_authentication_response(self):
64+
# parse authentication response
65+
query_string = flask.request.query_string.decode('utf-8')
66+
authn_resp = self.client.parse_response(AuthorizationResponse, info=query_string,
67+
sformat='urlencoded')
68+
69+
if authn_resp['state'] != flask.session['state']:
70+
raise ValueError('The \'state\' parameter does not match.')
71+
72+
# do token request
73+
args = {
74+
'code': authn_resp['code'],
75+
'redirect_uri': self.client.registration_response['redirect_uris'][0],
76+
'client_id': self.client.client_id,
77+
'client_secret': self.client.client_secret
78+
}
79+
token_resp = self.client.do_access_token_request(scope='openid', state=authn_resp['state'],
80+
request_args=args,
81+
authn_method='client_secret_basic')
82+
id_token = token_resp['id_token']
83+
if id_token['nonce'] != flask.session['nonce']:
84+
raise ValueError('The \'nonce\' parameter does not match.')
85+
access_token = token_resp['access_token']
86+
87+
# do userinfo request
88+
userinfo = self.client.do_user_info_request(state=authn_resp['state'])
89+
if userinfo['sub'] != id_token['sub']:
90+
raise ValueError('The \'sub\' of userinfo does not match \'sub\' of ID Token.')
91+
92+
# store the current user
93+
flask.g.id_token = id_token
94+
flask.g.access_token = access_token
95+
flask.g.userinfo = userinfo
96+
97+
return self.callback()
98+
99+
def oidc_auth(self, f):
100+
self.callback = f
101+
102+
@functools.wraps(f)
103+
def wrapper():
104+
return self._authenticate()
105+
106+
return wrapper

0 commit comments

Comments
 (0)