Skip to content

Commit 4083f60

Browse files
author
Rebecka Gulliksson
committed
Add an example application using pyOP.
1 parent af43d1f commit 4083f60

11 files changed

+282
-0
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ Uses [pyoidc](https://github.com/rohe/pyoidc/) and
77
# Provider implementations using pyOP
88
* [se-leg-op](https://github.com/SUNET/se-leg-op)
99
* [SATOSA OIDC frontend](https://github.com/its-dirg/SATOSA/blob/master/src/satosa/frontends/openid_connect.py)
10+
* [local example](example/views.py)
1011

1112
# Introduction
1213

example/README.md

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# pyOP example application
2+
To run the example application, execute the following commands:
3+
4+
```bash
5+
cd example/
6+
pip install -r requirements.txt # install the dependencies
7+
gunicorn wsgi:app -b :9090 --certfile https.crt --keyfile https.key # run the application
8+
```

example/app.py

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
from flask.app import Flask
2+
from flask.helpers import url_for
3+
from jwkest.jwk import RSAKey, rsa_load
4+
5+
from pyop.authz_state import AuthorizationState
6+
from pyop.provider import Provider
7+
from pyop.subject_identifier import HashBasedSubjectIdentifierFactory
8+
from pyop.userinfo import Userinfo
9+
10+
11+
def init_oidc_provider(app):
12+
with app.app_context():
13+
issuer = url_for('oidc_provider.index')[:-1]
14+
authentication_endpoint = url_for('oidc_provider.authentication_endpoint')
15+
jwks_uri = url_for('oidc_provider.jwks_uri')
16+
token_endpoint = url_for('oidc_provider.token_endpoint')
17+
userinfo_endpoint = url_for('oidc_provider.userinfo_endpoint')
18+
registration_endpoint = url_for('oidc_provider.registration_endpoint')
19+
end_session_endpoint = url_for('oidc_provider.end_session_endpoint')
20+
21+
configuration_information = {
22+
'issuer': issuer,
23+
'authorization_endpoint': authentication_endpoint,
24+
'jwks_uri': jwks_uri,
25+
'token_endpoint': token_endpoint,
26+
'userinfo_endpoint': userinfo_endpoint,
27+
'registration_endpoint': registration_endpoint,
28+
'end_session_endpoint': end_session_endpoint,
29+
'scopes_supported': ['openid', 'profile'],
30+
'response_types_supported': ['code', 'code id_token', 'code token', 'code id_token token'], # code and hybrid
31+
'response_modes_supported': ['query', 'fragment'],
32+
'grant_types_supported': ['authorization_code', 'implicit'],
33+
'subject_types_supported': ['pairwise'],
34+
'token_endpoint_auth_methods_supported': ['client_secret_basic'],
35+
'claims_parameter_supported': True
36+
}
37+
38+
userinfo_db = Userinfo(app.users)
39+
signing_key = RSAKey(key=rsa_load('signing_key.pem'), alg='RS256')
40+
provider = Provider(signing_key, configuration_information,
41+
AuthorizationState(HashBasedSubjectIdentifierFactory(app.config['SUBJECT_ID_HASH_SALT'])),
42+
{}, userinfo_db)
43+
44+
return provider
45+
46+
47+
def oidc_provider_init_app(name=None):
48+
name = name or __name__
49+
app = Flask(name)
50+
app.config.from_pyfile('application.cfg')
51+
52+
app.users = {'test_user': {'name': 'Testing Name'}}
53+
54+
from .views import oidc_provider_views
55+
app.register_blueprint(oidc_provider_views)
56+
57+
# Initialize the oidc_provider after views to be able to set correct urls
58+
app.provider = init_oidc_provider(app)
59+
60+
return app

example/application.cfg

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
SERVER_NAME = 'localhost:9090'
2+
SECRET_KEY = 'secret_key'
3+
SESSION_COOKIE_NAME='pyop_session'
4+
SUBJECT_ID_HASH_SALT = 'salt'
5+
PREFERRED_URL_SCHEME = 'https'

example/https.crt

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIIEBjCCAu6gAwIBAgIJAIybVu7kfIK0MA0GCSqGSIb3DQEBBQUAMF8xCzAJBgNV
3+
BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX
4+
aWRnaXRzIFB0eSBMdGQxGDAWBgNVBAMTD2xva2kuaXRzLnVtdS5zZTAeFw0xNTEy
5+
MTAxNDQyMDFaFw0yNTEyMDcxNDQyMDFaMF8xCzAJBgNVBAYTAkFVMRMwEQYDVQQI
6+
EwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQx
7+
GDAWBgNVBAMTD2xva2kuaXRzLnVtdS5zZTCCASIwDQYJKoZIhvcNAQEBBQADggEP
8+
ADCCAQoCggEBALiLDBwIteIobC+7JHoNeQRrTIbws9BghN4UUzyLo7+xeP9YwHaS
9+
tq6HqYK4cVLyx8k06Siw/4PwqMPNj9/B4f/ZXhEkXgbBP5TP36UgKrUIk4zInRFb
10+
Rjy+DcqjSZdgW1CKBKWJstXjSYen5rPm+voM/0msi164NPcfDMQIZmcQWh0MmEfG
11+
qlvdwTvjdaAQt8p7CGsxIdu4gPfhubknbTQKu+BVq5/RCVP7VU830PSr1RYhthX8
12+
Gt8ir32jEdDdjIrfA/zFx6PChyLkQFXtg/9WymnIM1j2ngNreL2nppwnqMYRnI9i
13+
C/y7MY4al3WeL9IETrtgh1jzXUNpgpJ03B0CAwEAAaOBxDCBwTAdBgNVHQ4EFgQU
14+
cTNphzIIRpQBQ2VT0Vx9xQYzNN0wgZEGA1UdIwSBiTCBhoAUcTNphzIIRpQBQ2VT
15+
0Vx9xQYzNN2hY6RhMF8xCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRl
16+
MSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxGDAWBgNVBAMTD2xv
17+
a2kuaXRzLnVtdS5zZYIJAIybVu7kfIK0MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcN
18+
AQEFBQADggEBAJYJfUqOPTyZ+tflKoN4l+scIXpBxqtQbjX+MYli6VHpl+M8y163
19+
KsCglXPddL7Z58KBrUDx1m6f7dFQ3PMYn/S2dUcRrNOdfaDKZ5QgyYj/iVr8HSOh
20+
6i1OtMFaBqW5WyqA5YgvUz63hZ2kDOBHZcEfSn2+roylBUiueV9gFNKDWneNMLo2
21+
PMZxcGWZ3wIQbu9ahakbJUvTigFStKeLoY1A2ZSTH7W4elB5DDxYOKZSzd/KZpfn
22+
/o/Pc7YbbEUYgIyf3QNusdH+t2pw9ZkrlKMhiv9ZAmjAWMDY7O/i3r7u7AkODu7z
23+
OHbH3rJqkbaiS8/q0cMZG6AMUzPRglzsTc4=
24+
-----END CERTIFICATE-----

example/https.key

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
-----BEGIN RSA PRIVATE KEY-----
2+
MIIEowIBAAKCAQEAuIsMHAi14ihsL7skeg15BGtMhvCz0GCE3hRTPIujv7F4/1jA
3+
dpK2roepgrhxUvLHyTTpKLD/g/Cow82P38Hh/9leESReBsE/lM/fpSAqtQiTjMid
4+
EVtGPL4NyqNJl2BbUIoEpYmy1eNJh6fms+b6+gz/SayLXrg09x8MxAhmZxBaHQyY
5+
R8aqW93BO+N1oBC3ynsIazEh27iA9+G5uSdtNAq74FWrn9EJU/tVTzfQ9KvVFiG2
6+
Ffwa3yKvfaMR0N2Mit8D/MXHo8KHIuRAVe2D/1bKacgzWPaeA2t4vaemnCeoxhGc
7+
j2IL/LsxjhqXdZ4v0gROu2CHWPNdQ2mCknTcHQIDAQABAoIBAQCooAWUqDDqUl1o
8+
z+voyt7FtvXaZ58mzMsb0h6suDwMMTKKwKI8tprOp4+wrrB+RvFfXUWftPwFp6XO
9+
JMtOfm7vxcM6jqyMJ5DdfYSx8c6UVR3eCoHbFjf70P3xJ3tbIuTNlw/f4w7Sejj6
10+
B+W6hVjXm4C55TwEdPWQyYJ0rehESxITn7DDjDNXxxwDqwAv8yPTYf8m7mb7qh3V
11+
EBnvZPIWpgEIVqV2crQSfHJwg29KhS7cnExJBYEPppBQ4aoUGyMqJN0EyBL4DrRo
12+
Ds6hPttLaXEmB+ACm/OQzhEeFKduob5OIKRSyp9Z8t6/B9uHiIKCENRU4O4zux57
13+
jZ8MIypRAoGBAO0anHYHlniyP5f/8u4kvTnW3wbDtRJ3L3zVSKN8shQmQnjNWmsI
14+
LhLWLU2OTRsXlJB7oYqNBojUBdeGimYot9kmjx4XkxELB6XAaYDG6pDvM7axC5qH
15+
iuC4jVHdEsIfy9dP6wZ/b+A+JOWWpS1vdAizfvgWI32JLGDPkjjUlzNvAoGBAMdA
16+
F5KZZLZFYsZM470/bb20qFMURDRI+5yz0VUHTNUQEr/xhMcYFske6ox14A7gXzwd
17+
SHAvTDkV8DsGu/FzSWzZmVhNc1EtdM3Cbe3Y7WJoIQoupuxBDEvrBE9riyOjW61q
18+
dYIO2ymfJfc2Vx6d7LAK9itXdqa3RIo7ZPK7EbMzAoGAAR1C5vsaJe8QhXJafewG
19+
R6NO4QVCcJfGzVtjQAFyBM45OcAdUKt1K/l9tQOaMSpnNFagZ7pJ8ZKthFnJhLlk
20+
Q8z+lzGdK1NV8d15oXVN3OiC4bTrTQqeCHhVkbDsSaVEm/pwLFOk/vTLz5hpplED
21+
xpaxXhEckZZ3cu0GzuWQ4FkCgYAb34tspqjAFtTKiNcTElx3vV4OwTcJWWxZb45J
22+
JsxIwgbdcxvv/h6x4/FL1PGTIzAvaKlJiFRRaBBDMZ35GPeckpQxFiSbppBAeIKI
23+
U2Bh888rbXtMcY0W0bm4ooLEaYXZrJrjptBh8jGNc7ycO9twhRgK2CFxERI1hDmK
24+
+0BuoQKBgHLRFdJYFkNKB+j56vTlB5AFTXcjs6x0dZ4j0SDAqVYgIHErpd2bhgOz
25+
s698rfdadwbYN4UEqvV/2NhDLx/jag4BvCermK71HaXvYnvqKw3UHGAG8K/dCCFR
26+
eSuZnxTSoWAPjMLi64hRzbWVT+oeZuy83lG6qDygbI1z1nlYWdo3
27+
-----END RSA PRIVATE KEY-----

example/requirements.txt

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
pyop
2+
Flask
3+
gunicorn

example/signing_key.pem

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
-----BEGIN RSA PRIVATE KEY-----
2+
MIICXAIBAAKBgQCi7nye2Ye1MrUD/sZAplfkpMkXHYduydvfvv/+Ihx1ClxKS/KG
3+
/1EhqyBVVvhHLRs9pimKMyLm2pBE51rGOt//XKhsoFAa37VID2iz7DQuV6DGgyBS
4+
FaKgaYBpinEQy2WcjU4eABnABV2r+K2UmGkqVJheqHqOqHUKasT4gy/6kQIDAQAB
5+
AoGAf7u+YX6ioNCvDwHHBVojn/H8YK3axmVkhiYkZWTysGM99VVTPridL2sMfzse
6+
jBZ1u8Av4tOyMg/5eLtz8+KmRjljpeAEFfsA1htWE8vESXnvDFwKldXD9Vi/kppb
7+
CYqASGCBUX3i1LPYffvjUxIgD+Tjx4k56c5EN5G331flDV0CQQDP8fWraegLJ+K1
8+
iXGNQzpaqG3EI3vf35Yb2bJpmD39QIXFIcJJ5MZHVW+1TyvgiavM4hS2+LGA8kGh
9+
OvMWfbYTAkEAyJWGBUmAW9mooo1Vw4tJjEWAHjHvzcQ4dqIju+WN8Xy5JTWkDD6Z
10+
VgKGtgLt2HfpSsgej14+Rh5mrjo4SbYxSwJAKG0syq9jOk/9xjc7STBJtvhJprkT
11+
SxnHsBBpnBfJ7WNO3l1KzVzZo2Kbvg7vQ87gBIvrZQsCT0RJuBOi0LuN2wJAAInm
12+
Qj1gSt7axRT8FfpZyDankW0w56yPOkJVNjv3lZ5wINl0B1RjtQdstTBs0xf/WGQR
13+
MPFf2XBbdjxRymDi4QJBAM3MUYPOlUk1UVCSQKyCkBwL3zMaPjBjD5LXkhGJzxsb
14+
T74NznwmCib/r0Rl/KmD7/bAq7R4aheOS/OMaZyhbkk=
15+
-----END RSA PRIVATE KEY-----

example/templates/logout.jinja2

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<!doctype html>
2+
<title>Logout</title>
3+
4+
Do you really want to logout?
5+
<form method="POST">
6+
<button type="submit" name="logout" value="logout">Yes, logout</button>
7+
<button type="submit" name="no_logout" value="no_logout">No, cancel logout</button>
8+
</form>

example/views.py

+124
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
from urllib.parse import urlencode, parse_qs
2+
3+
import flask
4+
from flask import Blueprint, redirect
5+
from flask import current_app
6+
from flask import jsonify
7+
from flask.helpers import make_response
8+
from flask.templating import render_template
9+
from oic.oic.message import TokenErrorResponse, UserInfoErrorResponse, EndSessionRequest
10+
11+
from pyop.access_token import AccessToken, BearerTokenError
12+
from pyop.exceptions import InvalidAuthenticationRequest, InvalidAccessToken, InvalidClientAuthentication, OAuthError, \
13+
InvalidSubjectIdentifier, InvalidClientRegistrationRequest
14+
from pyop.util import should_fragment_encode
15+
16+
oidc_provider_views = Blueprint('oidc_provider', __name__, url_prefix='')
17+
18+
19+
@oidc_provider_views.route('/')
20+
def index():
21+
return 'Hello world!'
22+
23+
24+
@oidc_provider_views.route('/registration', methods=['POST'])
25+
def registration_endpoint():
26+
try:
27+
response = current_app.provider.handle_client_registration_request(flask.request.get_data().decode('utf-8'))
28+
return make_response(jsonify(response.to_dict()), 201)
29+
except InvalidClientRegistrationRequest as e:
30+
return make_response(jsonify(e.to_dict()), status=400)
31+
32+
33+
@oidc_provider_views.route('/authentication', methods=['GET'])
34+
def authentication_endpoint():
35+
# parse authentication request
36+
try:
37+
auth_req = current_app.provider.parse_authentication_request(urlencode(flask.request.args),
38+
flask.request.headers)
39+
except InvalidAuthenticationRequest as e:
40+
current_app.logger.debug('received invalid authn request', exc_info=True)
41+
error_url = e.to_error_url()
42+
if error_url:
43+
return redirect(error_url, 303)
44+
else:
45+
# show error to user
46+
return make_response('Something went wrong: {}'.format(str(e)), 400)
47+
48+
# automagic authentication
49+
authn_response = current_app.provider.authorize(auth_req, 'test_user')
50+
response_url = authn_response.request(auth_req['redirect_uri'], should_fragment_encode(auth_req))
51+
return redirect(response_url, 303)
52+
53+
54+
@oidc_provider_views.route('/.well-known/openid-configuration')
55+
def provider_configuration():
56+
return jsonify(current_app.provider.provider_configuration.to_dict())
57+
58+
59+
@oidc_provider_views.route('/jwks')
60+
def jwks_uri():
61+
return jsonify(current_app.provider.jwks)
62+
63+
64+
@oidc_provider_views.route('/token', methods=['POST'])
65+
def token_endpoint():
66+
try:
67+
token_response = current_app.provider.handle_token_request(flask.request.get_data().decode('utf-8'),
68+
flask.request.headers)
69+
return jsonify(token_response.to_dict())
70+
except InvalidClientAuthentication as e:
71+
current_app.logger.debug('invalid client authentication at token endpoint', exc_info=True)
72+
error_resp = TokenErrorResponse(error='invalid_client', error_description=str(e))
73+
response = make_response(error_resp.to_json(), 401)
74+
response.headers['Content-Type'] = 'application/json'
75+
response.headers['WWW-Authenticate'] = 'Basic'
76+
return response
77+
except OAuthError as e:
78+
current_app.logger.debug('invalid request: %s', str(e), exc_info=True)
79+
error_resp = TokenErrorResponse(error=e.oauth_error, error_description=str(e))
80+
response = make_response(error_resp.to_json(), 400)
81+
response.headers['Content-Type'] = 'application/json'
82+
return response
83+
84+
85+
@oidc_provider_views.route('/userinfo', methods=['GET', 'POST'])
86+
def userinfo_endpoint():
87+
try:
88+
response = current_app.provider.handle_userinfo_request(flask.request.get_data().decode('utf-8'),
89+
flask.request.headers)
90+
return jsonify(response.to_dict())
91+
except (BearerTokenError, InvalidAccessToken) as e:
92+
error_resp = UserInfoErrorResponse(error='invalid_token', error_description=str(e))
93+
response = make_response(error_resp.to_json(), 401)
94+
response.headers['WWW-Authenticate'] = AccessToken.BEARER_TOKEN_TYPE
95+
response.headers['Content-Type'] = 'application/json'
96+
return response
97+
98+
99+
def do_logout(end_session_request):
100+
try:
101+
current_app.provider.logout_user(end_session_request=end_session_request)
102+
except InvalidSubjectIdentifier as e:
103+
return make_response('Logout unsuccessful!', 400)
104+
105+
redirect_url = current_app.provider.do_post_logout_redirect(end_session_request)
106+
if redirect_url:
107+
return redirect(redirect_url, 303)
108+
109+
return make_response('Logout successful!')
110+
111+
112+
@oidc_provider_views.route('/logout', methods=['GET', 'POST'])
113+
def end_session_endpoint():
114+
if flask.request.method == 'GET':
115+
# redirect from RP
116+
end_session_request = EndSessionRequest().deserialize(urlencode(flask.request.args))
117+
flask.session['end_session_request'] = end_session_request.to_dict()
118+
return render_template('logout.jinja2')
119+
else:
120+
form = parse_qs(flask.request.get_data().decode('utf-8'))
121+
if 'logout' in form:
122+
return do_logout(EndSessionRequest().from_dict(flask.session['end_session_request']))
123+
else:
124+
return make_response('You chose not to logout')

example/wsgi.py

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import logging
2+
3+
from example.app import oidc_provider_init_app
4+
5+
name = 'oidc_provider'
6+
app = oidc_provider_init_app(name)
7+
logging.basicConfig(level=logging.DEBUG)

0 commit comments

Comments
 (0)