canvas-lms/lib/authentication_methods.rb

326 lines
11 KiB
Ruby
Raw Normal View History

2011-02-01 09:57:29 +08:00
#
# Copyright (C) 2011 - 2013 Instructure, Inc.
2011-02-01 09:57:29 +08:00
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
#
module AuthenticationMethods
2011-02-01 09:57:29 +08:00
def authorized(*groups)
authorized_roles = groups
return true
end
2011-02-01 09:57:29 +08:00
def authorized_roles
@authorized_roles ||= []
end
2011-02-01 09:57:29 +08:00
def consume_authorized_roles
authorized_roles = []
end
def load_pseudonym_from_policy
if (policy_encoded = params['Policy']) &&
(signature = params['Signature']) &&
signature == Base64.encode64(OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha1'), Attachment.shared_secret, policy_encoded)).gsub(/\n/, '') &&
(policy = JSON.parse(Base64.decode64(policy_encoded)) rescue nil) &&
policy['conditions'] &&
(credential = policy['conditions'].detect{ |cond| cond.is_a?(Hash) && cond.has_key?("pseudonym_id") })
@policy_pseudonym_id = credential['pseudonym_id']
# so that we don't have to explicitly skip verify_authenticity_token
params[self.class.request_forgery_protection_token] ||= form_authenticity_token
end
yield if block_given?
end
class AccessTokenError < Exception
end
class LoggedOutError < Exception
end
def self.access_token(request, params_method = :params)
auth_header = request.authorization
if auth_header.present? && (header_parts = auth_header.split(' ', 2)) && header_parts[0] == 'Bearer'
header_parts[1]
else
request.send(params_method)['access_token'].presence
end
end
def self.user_id(request)
request.session[:user_id]
end
def load_pseudonym_from_access_token
return unless api_request? || (params[:controller] == 'oauth2_provider' && params[:action] == 'destroy')
token_string = AuthenticationMethods.access_token(request)
if token_string
@access_token = AccessToken.authenticate(token_string)
if !@access_token
raise AccessTokenError
2011-02-01 09:57:29 +08:00
end
if !@access_token.authorized_for_account?(@domain_root_account)
raise AccessTokenError
end
@current_user = @access_token.user
@current_pseudonym = @current_user.find_pseudonym_for_account(@domain_root_account, true)
unless @current_user && @current_pseudonym
raise AccessTokenError
end
@access_token.used!
RequestContextGenerator.add_meta_header('at', @access_token.global_id)
RequestContextGenerator.add_meta_header('dk', @access_token.global_developer_key_id) if @access_token.developer_key_id
end
end
def masked_authenticity_token
session_options = CanvasRails::Application.config.session_options
options = session_options.slice(:domain, :secure)
options[:httponly] = HostUrl.is_file_host?(request.host_with_port)
CanvasBreachMitigation::MaskingSecrets.masked_authenticity_token(cookies, options)
end
private :masked_authenticity_token
def load_user
@current_user = @current_pseudonym = nil
masked_authenticity_token # ensure that the cookie is set
load_pseudonym_from_access_token
if !@current_pseudonym
if @policy_pseudonym_id
@current_pseudonym = Pseudonym.where(id: @policy_pseudonym_id).first
elsif @pseudonym_session = PseudonymSession.find
@current_pseudonym = @pseudonym_session.record
# if the session was created before the last time the user explicitly
# logged out (of any session for any of their pseudonyms), invalidate
# this session
invalid_before = @current_pseudonym.user.last_logged_out
# they logged out in the future?!? something's busted; just ignore it -
# either my clock is off or whoever set this value's clock is off
invalid_before = nil if invalid_before && invalid_before > Time.now.utc
if invalid_before &&
(session_refreshed_at = request.env['encrypted_cookie_store.session_refreshed_at']) &&
session_refreshed_at < invalid_before
logger.info "Invalidating session: Session created before user logged out."
destroy_session
@current_pseudonym = nil
if api_request? || request.format.json?
raise LoggedOutError
end
end
if @current_pseudonym &&
session[:cas_session] &&
refactor PseudonymSessionsController fixes CNVS-20394 split it into appropriate concerns. main points are: * /login never renders a login form - it redirects forward to the default auth controller based on the first account authorization config (or discovery url on the account) * /login/canvas is the new home of the old login form. this form is never rendered in-situ anymore - other places that used to render it now redirect to /login (and then forward to here), reducing their knowledge of SSO * /login/ldap ends up at the same place (cause LDAP auth is handled transparently) * /login/cas and /login/saml redirect forward to the first SSO configuration of the appropriate type. /login/:auth_type/:id can be used to select a specific one * if an SSO fails, it redirects back to /login with flash[:error] set. this can forward to the discovery url appropriately, or render an error page appropriately (the old no_auto=1, but now it's not layered on top of the login partial that didn't show a login form) * ?canvas_login=1 is deprecated. just go directly to /login/canvas * /saml_consume, /saml_logout are deprecated. they are processed directly by /login/saml and /login/saml/logout * /login/:id is deprecated - it forwards to /login/:auth_type/:id as appropriate (presumably only saml, since that was the only one that previously should have been using these links) * OTP has been split into its own controller, and separated into multiple actions instead of one all-in-one action * /logout has been vastly simplified. the login controller should set session[:login_aac], and on logout it will check with that AAC for a url to redirect to after logout, instead of /login. SSO logout is handled by each controller if they support it test plan: * regression test the following functionality - * login with canvas auth * login with LDAP auth * login with SAML auth - and multiple SAMLs * login with CAS auth * MFA (configure, using, auto-setup) * Canvas as OAuth Provider flow * redirects to the login page when you're not logged in * failure of SAML/CAS (i.e. can't find user) show a decent error page and allows retry * "sticky" site admin auth (site admin is CAS/SAML, going directly to another domain logs you in with site admin) Change-Id: I1bb9d81a101939f812cbd5020e20749e883fdc0f Reviewed-on: https://gerrit.instructure.com/53220 QA-Review: August Thornton <august@instructure.com> Tested-by: Jenkins Reviewed-by: Ethan Vizitei <evizitei@instructure.com> Product-Review: Cody Cutrer <cody@instructure.com>
2015-05-01 03:58:57 +08:00
@current_pseudonym.cas_ticket_expired?(session[:cas_session])
logger.info "Invalidating session: CAS ticket expired - #{session[:cas_session]}."
destroy_session
@current_pseudonym = nil
raise LoggedOutError if api_request? || request.format.json?
redirect_to_login
end
end
if params[:login_success] == '1' && !@current_pseudonym
# they just logged in successfully, but we can't find the pseudonym now?
# sounds like somebody hates cookies.
return redirect_to(login_url(:needs_cookies => '1'))
end
@current_user = @current_pseudonym && @current_pseudonym.user
if api_request?
request.get? ||
!allow_forgery_protection ||
CanvasBreachMitigation::MaskingSecrets.valid_authenticity_token?(session, cookies, form_authenticity_param) ||
CanvasBreachMitigation::MaskingSecrets.valid_authenticity_token?(session, cookies, request.headers['X-CSRF-Token']) ||
raise(AccessTokenError)
end
2011-02-01 09:57:29 +08:00
end
2011-02-01 09:57:29 +08:00
if @current_user && @current_user.unavailable?
@current_pseudonym = nil
@current_user = nil
2011-02-01 09:57:29 +08:00
end
# required by the user throttling middleware
session[:user_id] = @current_user.global_id if @current_user
if @current_user && %w(become_user_id me become_teacher become_student).any? { |k| params.key?(k) }
request_become_user = nil
2011-02-01 09:57:29 +08:00
if params[:become_user_id]
request_become_user = User.where(id: params[:become_user_id]).first
2011-02-01 09:57:29 +08:00
elsif params.keys.include?('me')
request_become_user = @current_user
2011-02-01 09:57:29 +08:00
elsif params.keys.include?('become_teacher')
course = Course.find(params[:course_id] || params[:id]) rescue nil
teacher = course.teachers.first if course
if teacher
request_become_user = teacher
2011-02-01 09:57:29 +08:00
else
flash[:error] = I18n.t('lib.auth.errors.teacher_not_found', "No teacher found")
2011-02-01 09:57:29 +08:00
end
elsif params.keys.include?('become_student')
course = Course.find(params[:course_id] || params[:id]) rescue nil
student = course.students.first if course
if student
request_become_user = student
2011-02-01 09:57:29 +08:00
else
flash[:error] = I18n.t('lib.auth.errors.student_not_found', "No student found")
2011-02-01 09:57:29 +08:00
end
end
if request_become_user && request_become_user.id != session[:become_user_id].to_i && request_become_user.can_masquerade?(@current_user, @domain_root_account)
params_without_become = params.dup
params_without_become.delete_if {|k,v| [ 'become_user_id', 'become_teacher', 'become_student', 'me' ].include? k }
params_without_become[:only_path] = true
session[:masquerade_return_to] = url_for(params_without_become)
return redirect_to user_masquerade_url(request_become_user.id)
end
2011-02-01 09:57:29 +08:00
end
as_user_id = api_request? && params[:as_user_id].presence
as_user_id ||= session[:become_user_id]
if as_user_id
begin
user = api_find(User, as_user_id)
rescue ActiveRecord::RecordNotFound
end
if user && user.can_masquerade?(@current_user, @domain_root_account)
@real_current_user = @current_user
@current_user = user
@real_current_pseudonym = @current_pseudonym
@current_pseudonym = @current_user.find_pseudonym_for_account(@domain_root_account, true)
logger.warn "#{@real_current_user.name}(#{@real_current_user.id}) impersonating #{@current_user.name} on page #{request.url}"
elsif api_request?
# fail silently for UI, but not for API
render :json => {:errors => "Invalid as_user_id"}, :status => :unauthorized
return false
end
2011-02-01 09:57:29 +08:00
end
@current_user
end
private :load_user
2011-02-01 09:57:29 +08:00
def require_user
if @current_user && @current_pseudonym
true
else
redirect_to_login
false
2011-02-01 09:57:29 +08:00
end
end
protected :require_user
def clean_return_to(url)
return nil if url.blank?
begin
uri = URI.parse(url)
rescue URI::Error
return nil
end
return nil unless uri.path[0] == '/'
return "#{request.protocol}#{request.host_with_port}#{uri.path}#{uri.query && "?#{uri.query}"}#{uri.fragment && "##{uri.fragment}"}"
end
def return_to(url, fallback)
url = clean_return_to(url) || clean_return_to(fallback)
redirect_to url
end
2011-02-01 09:57:29 +08:00
def store_location(uri=nil, overwrite=true)
if overwrite || !session[:return_to]
uri ||= request.get? ? request.fullpath : request.referrer
session[:return_to] = clean_return_to(uri)
2011-02-01 09:57:29 +08:00
end
end
protected :store_location
def redirect_back_or_default(default)
redirect_to(session[:return_to] || default)
session.delete(:return_to)
2011-02-01 09:57:29 +08:00
end
protected :redirect_back_or_default
def redirect_to_referrer_or_default(default)
redirect_to(:back)
rescue ActionController::RedirectBackError
redirect_to(default)
end
def redirect_to_login
return unless fix_ms_office_redirects
respond_to do |format|
format.html {
store_location
flash[:warning] = I18n.t('lib.auth.errors.not_authenticated', "You must be logged in to access this page") unless request.path == '/'
redirect_to login_url(params.slice(:canvas_login, :authentication_provider))
}
format.json { render_json_unauthorized }
end
end
def render_json_unauthorized
add_www_authenticate_header if api_request? && !@current_user
if @current_user
render :json => {
:status => I18n.t('lib.auth.status_unauthorized', 'unauthorized'),
:errors => [{ :message => I18n.t('lib.auth.not_authorized', "user not authorized to perform that action") }]
},
:status => :unauthorized
else
render :json => {
:status => I18n.t('lib.auth.status_unauthenticated', 'unauthenticated'),
:errors => [{ :message => I18n.t('lib.auth.authentication_required', "user authorization required") }]
},
:status => :unauthorized
end
end
def add_www_authenticate_header
response['WWW-Authenticate'] = %{Bearer realm="canvas-lms"}
end
2011-02-01 09:57:29 +08:00
# Reset the session, and copy the specified keys over to the new session.
# Please consider the security implications of any keys you copy over.
def reset_session_saving_keys(*keys)
# can't use slice, because session has a different ctor than a normal hash
saved = {}
keys.each { |k| saved[k] = session[k] if session[k] }
2011-02-01 09:57:29 +08:00
reset_session
saved.each_pair { |k, v| session[k] = v }
2011-02-01 09:57:29 +08:00
end
refactor PseudonymSessionsController fixes CNVS-20394 split it into appropriate concerns. main points are: * /login never renders a login form - it redirects forward to the default auth controller based on the first account authorization config (or discovery url on the account) * /login/canvas is the new home of the old login form. this form is never rendered in-situ anymore - other places that used to render it now redirect to /login (and then forward to here), reducing their knowledge of SSO * /login/ldap ends up at the same place (cause LDAP auth is handled transparently) * /login/cas and /login/saml redirect forward to the first SSO configuration of the appropriate type. /login/:auth_type/:id can be used to select a specific one * if an SSO fails, it redirects back to /login with flash[:error] set. this can forward to the discovery url appropriately, or render an error page appropriately (the old no_auto=1, but now it's not layered on top of the login partial that didn't show a login form) * ?canvas_login=1 is deprecated. just go directly to /login/canvas * /saml_consume, /saml_logout are deprecated. they are processed directly by /login/saml and /login/saml/logout * /login/:id is deprecated - it forwards to /login/:auth_type/:id as appropriate (presumably only saml, since that was the only one that previously should have been using these links) * OTP has been split into its own controller, and separated into multiple actions instead of one all-in-one action * /logout has been vastly simplified. the login controller should set session[:login_aac], and on logout it will check with that AAC for a url to redirect to after logout, instead of /login. SSO logout is handled by each controller if they support it test plan: * regression test the following functionality - * login with canvas auth * login with LDAP auth * login with SAML auth - and multiple SAMLs * login with CAS auth * MFA (configure, using, auto-setup) * Canvas as OAuth Provider flow * redirects to the login page when you're not logged in * failure of SAML/CAS (i.e. can't find user) show a decent error page and allows retry * "sticky" site admin auth (site admin is CAS/SAML, going directly to another domain logs you in with site admin) Change-Id: I1bb9d81a101939f812cbd5020e20749e883fdc0f Reviewed-on: https://gerrit.instructure.com/53220 QA-Review: August Thornton <august@instructure.com> Tested-by: Jenkins Reviewed-by: Ethan Vizitei <evizitei@instructure.com> Product-Review: Cody Cutrer <cody@instructure.com>
2015-05-01 03:58:57 +08:00
# this really belongs on Login::Shared, but is left here for plugins that
# have always overridden it here
def delegated_auth_redirect_uri(uri)
uri
end
Register Parents and Add Observees when configured for SAML authentication Fixes PFS-1084 Parent Registration: When a Saml config is designated for Parent Registration the parent signing up will be redirected to a Saml login page where they will log in with their child's credentials. After login the child user's Saml session will be ended and the parent registration process will complete. Parent Adding Student: When a Saml config is designated for Parent Registration the parent adding another observee will be redirected to a Saml login page where they will log in with their child's credentials. After login the child user's Saml session will be ended and the observee creation process will complete. --------------------------------------- TEST PLAN: SETUP: 1) In your account settings check the box for 'Self Registration' (and either of the sub-options) 2) Add the following users to your account (these will be the students): billyjoel eltonjohn 3) In Authentication Settings add a SAML authentication service and enter the following fields (I've set up a remote SAML Idp): IdP Entity ID: http://107.170.212.143/saml2/idp/metadata.php Log On URL: http://107.170.212.143/simplesaml/saml2/idp/SSOService.php Log Out URL: http://107.170.212.143/simplesaml/saml2/idp/SingleLogoutService.php Certificate Fingerprint: 9C:11:68:93:95:CD:18:01:EC:52:2B:9E:22:7F:73:55:ED:6D:82:D4 Parent Registration: check TEST: Parent Registration: * Go to '/login/canvas' * Click on the signup banner * sign up as a parent for billyjoel or eltonjohn (on SAML login page the password for either user is: tantrum) Add Student: * Log in as a parent user w/ a Canvas Auth login * Go to '/profile/observees' * Add Student 'billyjoel' or 'eltonjohn' Authentication Settings (new parent reg checkbox): * Go to Authentication Settings * Add a second SAML config * check the parent registration checkbox - it should warn that selection will deselect the other and in fact do so upon save. - the selected config is the one used for parent reg/add student --------------------------------------- Change-Id: Ief83b604fc252c88dbb912c56de65d8620fe802f Reviewed-on: https://gerrit.instructure.com/49691 Tested-by: Jenkins QA-Review: August Thornton <august@instructure.com> Reviewed-by: Jacob Fugal <jacob@instructure.com> Product-Review: Ethan Vizitei <evizitei@instructure.com>
2015-03-03 01:41:17 +08:00
2011-02-01 09:57:29 +08:00
end