Skip to content

Commit

Permalink
Merge pull request #512 from ubc/master-#509
Browse files Browse the repository at this point in the history
Improve CAS authentication
  • Loading branch information
xcompass authored Mar 2, 2017
2 parents c908823 + 61f2d47 commit 47b2178
Show file tree
Hide file tree
Showing 13 changed files with 326 additions and 129 deletions.
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ xAPI statements require an actor (currently logged in user) account information.

`LRS_ACTOR_ACCOUNT_CAS_HOMEPAGE`: Set the homepage of the CAS account

`LRS_ACTOR_ACCOUNT_CAS_IDENTIFIER`: Optionally set a param to set as the actor's unique key for the CAS account. Requires `CAS_ATTRIBUTES_TO_STORE` to be set when not using default setting. (uses CAS username by default)
`LRS_ACTOR_ACCOUNT_CAS_IDENTIFIER`: Optionally set a param to set as the actor's unique key for the CAS account. (uses CAS username by default)

Restart server after making any changes to settings

Expand Down Expand Up @@ -144,9 +144,11 @@ Restart server after making any changes to settings

`CAS_LOGIN_ENABLED`: Enable login via CAS server (default: True)

`CAS_ATTRIBUTES_TO_STORE`: Array of CAS attributes to store in the third_party_user table's param column. (default: empty)
`CAS_SERVER`: Url of the CAS Server (do not include trailing slash)

See [Flask-CAS](https://github.com/cameronbwhite/Flask-CAS) for other CAS settings
`CAS_AUTH_PREFIX`: Prefix to CAS action (default '/cas')

`CAS_USE_SAML`: Determines which authorization endpoint to use. '/serviceValidate' if false (default). '/samlValidate' if true.

Restart server after making any changes to settings

Expand Down
7 changes: 4 additions & 3 deletions compair/__init__.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import json
import os
import ssl
import requests
from requests.packages.urllib3.exceptions import InsecureRequestWarning

from flask import Flask, redirect, session as sess, abort, jsonify, url_for
from flask_login import current_user
from sqlalchemy.orm import joinedload
from werkzeug.routing import BaseConverter

from .authorization import define_authorization
from .core import login_manager, bouncer, db, cas, celery
from .core import login_manager, bouncer, db, celery
from .configuration import config
from .models import User, File
from .activity import log
Expand Down Expand Up @@ -75,6 +77,7 @@ def create_app(conf=config, settings_override=None, skip_endpoints=False, skip_a
else:
# Handle target environment that doesn't support HTTPS verification
ssl._create_default_https_context = _create_unverified_https_context
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)

app.logger.debug("Application Configuration: " + str(app.config))

Expand Down Expand Up @@ -110,8 +113,6 @@ def unauthorized():
return response
return abort(401)

cas.init_app(app)

# Flask-Bouncer initialization
bouncer.init_app(app)

Expand Down
116 changes: 70 additions & 46 deletions compair/api/login.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@

from flask import Blueprint, jsonify, request, session as sess, current_app, url_for, redirect, Flask, render_template
from flask_login import current_user, login_required, login_user, logout_user
from compair import cas
from compair.core import db, event
from compair.authorization import get_logged_in_user_permissions
from compair.models import User, LTIUser, LTIResourceLink, LTIUserResourceLink, UserCourse, LTIContext, \
ThirdPartyUser, ThirdPartyType
from compair.cas import get_cas_login_url, validate_cas_ticket, get_cas_logout_url

login_api = Blueprint("login_api", __name__, url_prefix='/api')

Expand Down Expand Up @@ -71,7 +71,7 @@ def logout():
url = jsonify({'redirect': return_url})

elif sess.get('CAS_LOGIN'):
url = jsonify({'redirect': url_for('cas.logout')})
url = jsonify({'redirect': url_for('login_api.cas_logout')})

sess.clear()
return url
Expand All @@ -88,68 +88,92 @@ def session():
def get_permission():
return jsonify(get_logged_in_user_permissions())

@login_api.route('/cas/login')
def cas_login():
if not current_app.config.get('CAS_LOGIN_ENABLED'):
return "", 403

return redirect(get_cas_login_url())

@login_api.route('/auth/cas', methods=['GET'])
def auth_cas():
@login_api.route('/cas/auth', methods=['GET'])
def cas_auth():
"""
CAS Authentication Endpoint. Authenticate user through CAS. If user doesn't exists,
set message in session so that frontend can get the message through /session call
"""
if not current_app.config.get('CAS_LOGIN_ENABLED'):
return "", 403

username = cas.username
url = "/app/#/lti" if sess.get('LTI') else "/"
error_message = None
ticket = request.args.get("ticket")

if username is not None:
thirdpartyuser = ThirdPartyUser.query. \
filter_by(
unique_identifier=username,
third_party_type=ThirdPartyType.cas
) \
.one_or_none()
msg = None

# store additional CAS attributes if needed
additional_params = None
if cas.attributes and len(current_app.config.get('CAS_ATTRIBUTES_TO_STORE')) > 0:
additional_params = {}
for attr_name in current_app.config.get('CAS_ATTRIBUTES_TO_STORE'):
additional_params[attr_name] = cas.attributes.get('cas:'+attr_name)

if not thirdpartyuser or not thirdpartyuser.user:
if sess.get('LTI') and sess.get('oauth_create_user_link'):
sess['CAS_CREATE'] = True
sess['CAS_UNIQUE_IDENTIFIER'] = cas.username
sess['CAS_ADDITIONAL_PARAMS'] = additional_params
url = '/app/#/oauth/create'
else:
current_app.logger.debug("Login failed, invalid username for: " + username)
msg = 'You don\'t have access to this application.'
# check if token isn't present
if not ticket:
error_message = "No token in request"
else:
validation_response = validate_cas_ticket(ticket)

if not validation_response.success:
current_app.logger.debug(
"CAS Server did NOT validate ticket:%s and included this response:%s" % (ticket, validation_response.response)
)
error_message = "Login Failed. CAS ticket was invalid."
elif not validation_response.user:
current_app.logger.debug("CAS Server responded with valid ticket but no user")
error_message = "Login Failed. Expecting CAS username to be set."
else:
authenticate(thirdpartyuser.user, login_method=thirdpartyuser.third_party_type.value)
thirdpartyuser.params = additional_params
current_app.logger.debug(
"CAS Server responded with user:%s and attributes:%s" % (validation_response.user, validation_response.attributes)
)
username = validation_response.user

thirdpartyuser = ThirdPartyUser.query. \
filter_by(
unique_identifier=username,
third_party_type=ThirdPartyType.cas
) \
.one_or_none()

# store additional CAS attributes if needed
if not thirdpartyuser or not thirdpartyuser.user:
if sess.get('LTI') and sess.get('oauth_create_user_link'):
sess['CAS_CREATE'] = True
sess['CAS_UNIQUE_IDENTIFIER'] = username
sess['CAS_PARAMS'] = validation_response.attributes
url = '/app/#/oauth/create'
else:
current_app.logger.debug("Login failed, invalid username for: " + username)
error_message = "You don't have access to this application."
else:
authenticate(thirdpartyuser.user, login_method=thirdpartyuser.third_party_type.value)
thirdpartyuser.params = validation_response.attributes

if sess.get('LTI') and sess.get('oauth_create_user_link'):
lti_user = LTIUser.query.get_or_404(sess['lti_user'])
lti_user.compair_user_id = thirdpartyuser.user_id
sess.pop('oauth_create_user_link')
if sess.get('LTI') and sess.get('oauth_create_user_link'):
lti_user = LTIUser.query.get_or_404(sess['lti_user'])
lti_user.compair_user_id = thirdpartyuser.user_id
sess.pop('oauth_create_user_link')

if sess.get('LTI') and sess.get('lti_context') and sess.get('lti_user_resource_link'):
lti_context = LTIContext.query.get_or_404(sess['lti_context'])
lti_user_resource_link = LTIUserResourceLink.query.get_or_404(sess['lti_user_resource_link'])
lti_context.update_enrolment(thirdpartyuser.user_id, lti_user_resource_link.course_role)
if sess.get('LTI') and sess.get('lti_context') and sess.get('lti_user_resource_link'):
lti_context = LTIContext.query.get_or_404(sess['lti_context'])
lti_user_resource_link = LTIUserResourceLink.query.get_or_404(sess['lti_user_resource_link'])
lti_context.update_enrolment(thirdpartyuser.user_id, lti_user_resource_link.course_role)

db.session.commit()
sess['CAS_LOGIN'] = True
else:
msg = 'Login Failed. Expecting CAS username to be set.'
db.session.commit()
sess['CAS_LOGIN'] = True

if msg is not None:
sess['CAS_AUTH_MSG'] = msg
if error_message is not None:
sess['CAS_AUTH_MSG'] = error_message

return redirect(url)

@login_api.route('/cas/logout', methods=['GET'])
def cas_logout():
if not current_app.config.get('CAS_LOGIN_ENABLED'):
return "", 403

return redirect(get_cas_logout_url())


def authenticate(user, login_method=None):
# username valid, password valid, login successful
Expand Down
2 changes: 1 addition & 1 deletion compair/api/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ def post(self):
thirdpartyuser = ThirdPartyUser(
third_party_type=ThirdPartyType.cas,
unique_identifier=sess.get('CAS_UNIQUE_IDENTIFIER'),
params=sess.get('CAS_ADDITIONAL_PARAMS'),
params=sess.get('CAS_PARAMS'),
user=user
)
login_method = ThirdPartyType.cas.value
Expand Down
Loading

0 comments on commit 47b2178

Please sign in to comment.