-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Added SAML code; factored views into separate file
- Loading branch information
1 parent
bc4da2c
commit cc060d9
Showing
6 changed files
with
326 additions
and
52 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
from flask import current_app | ||
|
||
def prepare_token_payload(code: str): | ||
''' | ||
:param code: the code returned from ORCID after the user authorizes our application. | ||
''' | ||
app = current_app._get_current_object() | ||
return {'client_id': app.config['CLIENT_ID'], | ||
'client_secret': app.config['CLIENT_SECRET'], | ||
'grant_type': 'authorization_code', | ||
'code': code, | ||
'redirect_uri': url_for('orcid_redirect', _external=True)} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
''' | ||
Functions to handle the creation of SAML SP metadata and to retrieve attributes from the IdP metadata. | ||
The following code closely follows the Flask example at https://github.com/onelogin/python3-saml/blob/master/demo-flask/index.py | ||
''' | ||
|
||
from onelogin.saml2.auth import OneLogin_Saml2_Auth | ||
from onelogin.saml2.utils import OneLogin_Saml2_Utils | ||
from flask import current_app | ||
from urllib.parse import urlparse | ||
|
||
def init_saml_auth(request): | ||
''' | ||
:params request: a Flask request object | ||
returns a python-saml auth object and the modified request object | ||
''' | ||
# Get current Flask app (to access configurations) | ||
app = current_app._get_current_object() | ||
auth_req = prepare_flask_request(request) | ||
auth = OneLogin_Saml2_Auth(auth_req, custom_base_path=app.config['SAML_PATH']) | ||
return auth, auth_req | ||
|
||
def prepare_flask_request(request): | ||
''' | ||
:param request: A Flask request object | ||
returns a python-saml request object with data from the Flask request object | ||
''' | ||
# host of the SP | ||
url_data = urlparse(request.url) | ||
return { | ||
'https': 'on' if request.scheme == 'https' else 'off', | ||
'http_host': request.host, | ||
'server_port': url_data.port, | ||
'script_name': request.path, | ||
'get_data': request.args.copy(), | ||
# Uncomment if using ADFS as IdP, https://github.com/onelogin/python-saml/pull/144 | ||
# 'lowercase_urlencoding': True, | ||
'post_data': request.form.copy() | ||
} | ||
|
||
def get_metadata_from_session(session): | ||
''' | ||
Extracts values from a Flask session object corresponding to certain SAML metadata attributes. | ||
:param session: a Flask session object | ||
''' | ||
# SAML attribute names | ||
attr_names = ['samlNameId', 'samlSessionIndex', 'samlNameIdFormat', 'samlNameIdNameQualifier', 'samlNameIdSPNameQualifier'] | ||
# parameter names used by the python3-saml auth object | ||
keys = ['name_id', 'session_index', 'name_id_format', 'nq', 'spqn'] | ||
metadata_dict = {key: session.get(attr_name) for (key, attr_name) in zip(keys, attr_names)} | ||
return metadata_dict | ||
|
||
def add_metadata_to_session(auth, session): | ||
''' | ||
Stores the SAML metadata in the IdP response in a session object. | ||
:param auth: a python-saml auth object. | ||
:param session: a Flask session object. | ||
''' | ||
# Delete the previous session ID if storing this | ||
if 'AuthNRequestID' in session: | ||
del session['AuthNRequestID'] | ||
# Store SAML attributes in the session object | ||
session['samlUserdata'] = auth.get_attributes() | ||
session['samlNameId'] = auth.get_nameid() | ||
session['samlNameIdFormat'] = auth.get_nameid_format() | ||
session['samlNameIdNameQualifier'] = auth.get_nameid_nq() | ||
session['samlNameIdSPNameQualifier'] = auth.get_nameid_spnq() | ||
session['samlSessionIndex'] = auth.get_session_index() | ||
|
||
return session | ||
|
||
def get_attributes(session): | ||
''' | ||
Retrieve SAML attributes from Flask session data. | ||
:param session: a Flask session object | ||
returns attributes (if any) and a Boolean flag for the logout button --> Not sure we need this | ||
''' | ||
if 'samlUserdata' in session: | ||
if len(session['samlUserdata']) > 0: | ||
attributes = session['samlUserdata'].items() | ||
return attributes, True | ||
return False, False |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
from flask import request, url_for, redirect, session | ||
from orcidflask import app | ||
from saml_utils import * | ||
from onelogin.saml2.utils import OneLogin_Saml2_Utils | ||
import requests | ||
from requests.exceptions import HTTPError | ||
|
||
@app.route('/login', methods=['GET', 'POST']) | ||
def login(): | ||
''' | ||
Route handles the SSO process | ||
''' | ||
auth, auth_req = init_saml_auth(request) | ||
errors = [] | ||
error_reason = None | ||
not_auth_warn = False | ||
success_slo = False | ||
attributes = False | ||
paint_logout = False | ||
|
||
# Initiating the SSO process | ||
if 'sso' in request.args: | ||
return redirect(auth.login()) | ||
# Initiating the SLO process | ||
elif 'slo' in request.args: | ||
metadata = get_metadata_from_session(session) | ||
return redirect(auth.logout(**metadata)) | ||
|
||
# Consuming the SAML attributes from the IdP | ||
if 'acs' in request.args: | ||
# Getting the request ID, if necessary | ||
request_id = None | ||
if 'AuthNRequestID' in session: | ||
request_id = session['AuthNRequestID'] | ||
# Process the XML | ||
auth.process_response(request_id=request_id) | ||
errors = auth.get_errors() | ||
# Check for errors | ||
not_auth_warn = not auth.is_authenticated() | ||
if len(errors) == 0: | ||
# Update the Flask session object with the SAML attributes for this user | ||
# Updating by reference here; otherwise, we break the session context local | ||
add_metadata_to_session(auth, session) | ||
# Redirect to a new page, if necessary | ||
self_url = OneLogin_Saml2_Utils.get_self_url(auth_req) | ||
if 'RelayState' in request.form and self_url != request.form['RelayState']: | ||
return redirect(auth.redirect_to(request.form['RelayState'])) | ||
# Get the reason for auth failure if exists | ||
elif auth.get_settings().is_debug_active(): | ||
error_reason = auth.get_last_error_reason() | ||
|
||
# Handle logout | ||
elif 'sls' in request.args: | ||
request_id = None | ||
if 'LogoutRequestID' in session: | ||
request_id = session['LogoutRequestID'] | ||
dscb = lambda: session.clear() | ||
url = auth.process_slo(request_id=request_id, delete_session_cb=dscb) | ||
errors = auth.get_errors() | ||
if len(errors) == 0: | ||
if url is not None: | ||
return redirect(url) | ||
else: | ||
success_slo = True | ||
elif auth.get_settings().is_debug_active(): | ||
error_reason = auth.get_last_error_reason() | ||
|
||
attributes, paint_logout = get_attributes(session) | ||
|
||
return render_template( | ||
'index.html', | ||
errors=errors, | ||
error_reason=error_reason, | ||
not_auth_warn=not_auth_warn, | ||
success_slo=success_slo, | ||
attributes=attributes, | ||
paint_logout=paint_logout | ||
) | ||
|
||
@app.route('/attrs/') | ||
def attrs(): | ||
attributes, paint_logout = get_attributes(session) | ||
return render_template('attrs.html', paint_logout=paint_logout, | ||
attributes=attributes) | ||
|
||
|
||
@app.route('/metadata/') | ||
def metadata(): | ||
req = prepare_flask_request(request) | ||
auth = init_saml_auth(req) | ||
settings = auth.get_settings() | ||
metadata = settings.get_sp_metadata() | ||
errors = settings.validate_metadata(metadata) | ||
|
||
if len(errors) == 0: | ||
resp = make_response(metadata, 200) | ||
resp.headers['Content-Type'] = 'text/xml' | ||
else: | ||
resp = make_response(', '.join(errors), 500) | ||
return resp | ||
|
||
|
||
@app.route('/') | ||
def index(): | ||
''' | ||
Should render homepage and if behind SSO, retrieve netID from SAML and store in a session variable. | ||
See the example here: https://github.com/onelogin/python3-saml/blob/master/demo-flask/index.py | ||
''' | ||
# TO DO: Add template with button to take user to the ORCID authorization site | ||
# TO DO: Capture SAML identifier in session object | ||
return redirect(app.config['orcid_auth_url'].format(orcid_client_id=app.config['CLIENT_ID'], | ||
redirect_uri=url_for('orcid_redirect', | ||
_external=True))) | ||
|
||
@app.route('/orcid-redirect') | ||
def orcid_redirect(): | ||
''' | ||
Redirect route that retrieves the one-time code from ORCID after user logs in and approves. | ||
''' | ||
# TO DO: Check for error/access denied | ||
orcid_code = request.args.get('code') | ||
headers = {'Accept': 'application/json', | ||
'Content-Type': 'application/x-www-form-urlencoded'} | ||
try: | ||
response = requests.post(app.config['orcid_token_url'], | ||
headers=headers, | ||
data=prepare_token_payload(orcid_code)) | ||
response.raise_for_status() | ||
except HTTPError as e: | ||
# TO DO: handle HTTP errors | ||
raise | ||
orcid_auth = response.json() | ||
print(orcid_auth) | ||
# TO DO: Retrieve SAML identifier from session object | ||
# TO DO: Save to data store | ||
# TO DO: return success template | ||
return "Successfully authorized!" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
''' | ||
Functions to handle the creation of SAML SP metadata and to retrieve attributes from the IdP metadata. | ||
The following code closely follows the Flask example at https://github.com/onelogin/python3-saml/blob/master/demo-flask/index.py | ||
''' | ||
|
||
from onelogin.saml2.auth import OneLogin_Saml2_Auth | ||
from onelogin.saml2.utils import OneLogin_Saml2_Utils | ||
from flask import current_app | ||
from urllib.parse import urlparse | ||
|
||
def init_saml_auth(request): | ||
''' | ||
:params request: a Flask request object | ||
returns a python-saml auth object and the modified request object | ||
''' | ||
# Get current Flask app (to access configurations) | ||
app = current_app._get_current_object() | ||
auth_req = prepare_flask_request(request) | ||
auth = OneLogin_Saml2_Auth(auth_req, custom_base_path=app.config['SAML_PATH']) | ||
return auth, auth_req | ||
|
||
def prepare_flask_request(request): | ||
''' | ||
:param request: A Flask request object | ||
returns a python-saml request object with data from the Flask request object | ||
''' | ||
# host of the SP | ||
url_data = urlparse(request.url) | ||
return { | ||
'https': 'on' if request.scheme == 'https' else 'off', | ||
'http_host': request.host, | ||
'server_port': url_data.port, | ||
'script_name': request.path, | ||
'get_data': request.args.copy(), | ||
# Uncomment if using ADFS as IdP, https://github.com/onelogin/python-saml/pull/144 | ||
# 'lowercase_urlencoding': True, | ||
'post_data': request.form.copy() | ||
} | ||
|
||
def get_metadata_from_session(session): | ||
''' | ||
Extracts values from a Flask session object corresponding to certain SAML metadata attributes. | ||
:param session: a Flask session object | ||
''' | ||
# SAML attribute names | ||
attr_names = ['samlNameId', 'samlSessionIndex', 'samlNameIdFormat', 'samlNameIdNameQualifier', 'samlNameIdSPNameQualifier'] | ||
# parameter names used by the python3-saml auth object | ||
keys = ['name_id', 'session_index', 'name_id_format', 'nq', 'spqn'] | ||
metadata_dict = {key: session.get(attr_name) for (key, attr_name) in zip(keys, attr_names)} | ||
return metadata_dict | ||
|
||
def add_metadata_to_session(auth, session): | ||
''' | ||
Stores the SAML metadata in the IdP response in a session object. | ||
:param auth: a python-saml auth object. | ||
:param session: a Flask session object. | ||
''' | ||
# Delete the previous session ID if storing this | ||
if 'AuthNRequestID' in session: | ||
del session['AuthNRequestID'] | ||
# Store SAML attributes in the session object | ||
session['samlUserdata'] = auth.get_attributes() | ||
session['samlNameId'] = auth.get_nameid() | ||
session['samlNameIdFormat'] = auth.get_nameid_format() | ||
session['samlNameIdNameQualifier'] = auth.get_nameid_nq() | ||
session['samlNameIdSPNameQualifier'] = auth.get_nameid_spnq() | ||
session['samlSessionIndex'] = auth.get_session_index() | ||
|
||
return session | ||
|
||
def get_attributes(session): | ||
''' | ||
Retrieve SAML attributes from Flask session data. | ||
:param session: a Flask session object | ||
returns attributes (if any) and a Boolean flag for the logout button --> Not sure we need this | ||
''' | ||
if 'samlUserdata' in session: | ||
if len(session['samlUserdata']) > 0: | ||
attributes = session['samlUserdata'].items() | ||
return attributes, True | ||
return False, False |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
from setuptools import setup | ||
|
||
setup( | ||
name='orcidflask', | ||
packages=['orcidflask'], | ||
include_package_data=True, | ||
install_requires=[ | ||
'flask', | ||
], | ||
) |