Google and OpenID Connect auth backends
closes CNVS-19536, CNVS-19540, CNVS-19545 test plan: * test google auth * re-test the other oauth providers, cause more stuff got refactored Change-Id: Ib1c0332cc31f0825f171f3281bf7255abb602844 Reviewed-on: https://gerrit.instructure.com/54526 Tested-by: Jenkins QA-Review: August Thornton <august@instructure.com> Reviewed-by: Ethan Vizitei <evizitei@instructure.com> Product-Review: Cody Cutrer <cody@instructure.com>
This commit is contained in:
parent
ddcfaa948e
commit
9ec7c4ee0c
|
@ -179,14 +179,15 @@ class AccountAuthorizationConfigsController < ApplicationController
|
|||
# @API Create Authorization Config
|
||||
#
|
||||
# Add external account authentication service(s) for the account.
|
||||
# Services may be CAS, Facebook, GitHub, LDAP, LinkedIn, SAML, or Twitter.
|
||||
# Services may be CAS, Facebook, GitHub, Google, LDAP, OpenID Connect,
|
||||
# LinkedIn, SAML, or Twitter.
|
||||
#
|
||||
# Each authentication service is specified as a set of parameters as
|
||||
# described below. A service specification must include an 'auth_type'
|
||||
# parameter with a value of 'cas', 'facebook', 'github', 'ldap', 'linkedin',
|
||||
# 'saml', or 'twitter'. The other recognized parameters depend on this
|
||||
# auth_type; unrecognized parameters are discarded. Service specifications
|
||||
# not specifying a valid auth_type are ignored.
|
||||
# parameter with a value of 'cas', 'facebook', 'github', 'google', 'ldap',
|
||||
# 'linkedin', 'openid_connect', 'saml', or 'twitter'. The other recognized
|
||||
# parameters depend on this auth_type; unrecognized parameters are discarded.
|
||||
# Service specifications not specifying a valid auth_type are ignored.
|
||||
#
|
||||
# _Deprecated_[2015-05-08] Any service specification may include an
|
||||
# optional 'login_handle_name' parameter. This parameter specifies the
|
||||
|
@ -244,6 +245,18 @@ class AccountAuthorizationConfigsController < ApplicationController
|
|||
# The GitHub application's Client Secret. Not available if configured
|
||||
# globally for Canvas.
|
||||
#
|
||||
# For Google, the additional recognized parameters are:
|
||||
#
|
||||
# - client_id [Required]
|
||||
#
|
||||
# The Google application's Client ID. Not available if configured globally
|
||||
# for Canvas.
|
||||
#
|
||||
# - client_secret [Required]
|
||||
#
|
||||
# The Google application's Client Secret. Not available if configured
|
||||
# globally for Canvas.
|
||||
#
|
||||
# For LDAP authentication services, the additional recognized parameters are:
|
||||
#
|
||||
# - auth_host
|
||||
|
@ -299,6 +312,25 @@ class AccountAuthorizationConfigsController < ApplicationController
|
|||
# The LinkedIn application's Client Secret. Not available if configured
|
||||
# globally for Canvas.
|
||||
#
|
||||
# For OpenID Connect, the additional recognized parameters are:
|
||||
#
|
||||
# - client_id [Required]
|
||||
#
|
||||
# The application's Client ID.
|
||||
#
|
||||
# - client_secret [Required]
|
||||
#
|
||||
# The application's Client Secret.
|
||||
#
|
||||
# - authorize_url [Required]
|
||||
#
|
||||
# The URL for getting starting the OAuth 2.0 web flow
|
||||
#
|
||||
# - token_url [Required]
|
||||
#
|
||||
# The URL for exchanging the OAuth 2.0 authorization code for an access
|
||||
# token and id token
|
||||
#
|
||||
# For SAML authentication services, the additional recognized parameters are:
|
||||
#
|
||||
# - idp_entity_id
|
||||
|
|
|
@ -1887,7 +1887,12 @@ class ApplicationController < ActionController::Base
|
|||
end
|
||||
|
||||
def google_drive_client(refresh_token=nil, access_token=nil)
|
||||
client_secrets = Canvas::Plugin.find(:google_drive).try(:settings)
|
||||
settings = Canvas::Plugin.find(:google_drive).try(:settings) || {}
|
||||
client_secrets = {
|
||||
client_id: settings[:client_id],
|
||||
client_secret: settings[:client_secret_dec],
|
||||
redirect_uri: settings[:redirect_uri]
|
||||
}.with_indifferent_access
|
||||
GoogleDrive::Client.create(client_secrets, refresh_token, access_token)
|
||||
end
|
||||
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
#
|
||||
# Copyright (C) 2015 Instructure, Inc.
|
||||
#
|
||||
# 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 Login
|
||||
GoogleController = Oauth2Controller
|
||||
end
|
|
@ -19,19 +19,24 @@
|
|||
class Login::Oauth2Controller < Login::OauthBaseController
|
||||
def new
|
||||
super
|
||||
state = session[:oauth2_state] = SecureRandom.hex(24)
|
||||
redirect_to @aac.authorize_url(redirect_uri, state)
|
||||
nonce = session[:oauth2_nonce] = SecureRandom.hex(24)
|
||||
jwt = Canvas::Security.create_jwt(aac_id: @aac.global_id, nonce: nonce)
|
||||
redirect_to @aac.generate_authorize_url(oauth2_login_callback_url, jwt)
|
||||
end
|
||||
|
||||
def create
|
||||
super
|
||||
raise ActiveRecord::RecordNotFound unless @aac.is_a?(AccountAuthorizationConfig::Oauth2)
|
||||
reset_session_for_login
|
||||
|
||||
check_csrf
|
||||
if jwt['nonce'] != session.delete(:oauth2_nonce)
|
||||
raise ActionController::InvalidAuthenticityToken
|
||||
end
|
||||
|
||||
@aac = AccountAuthorizationConfig.find(jwt['aac_id'])
|
||||
raise ActiveRecord::RecordNotFound unless @aac.is_a?(AccountAuthorizationConfig::Oauth2)
|
||||
|
||||
unique_id = nil
|
||||
return unless timeout_protection do
|
||||
token = @aac.get_token(params[:code], redirect_uri)
|
||||
token = @aac.get_token(params[:code], oauth2_login_callback_url)
|
||||
unique_id = @aac.unique_id(token)
|
||||
end
|
||||
|
||||
|
@ -40,13 +45,11 @@ class Login::Oauth2Controller < Login::OauthBaseController
|
|||
|
||||
protected
|
||||
|
||||
def check_csrf
|
||||
if params[:state].blank? || params[:state] != session.delete(:oauth2_state)
|
||||
raise ActionController::InvalidAuthenticityToken
|
||||
end
|
||||
end
|
||||
|
||||
def redirect_uri
|
||||
oauth2_login_callback_url(id: @aac)
|
||||
def jwt
|
||||
@jwt ||= if params[:state].present?
|
||||
Canvas::Security.decode_jwt(params[:state])
|
||||
else
|
||||
{}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -37,12 +37,6 @@ class Login::OauthBaseController < ApplicationController
|
|||
reset_session_for_login
|
||||
end
|
||||
|
||||
def create
|
||||
reset_session_for_login
|
||||
|
||||
@aac = @domain_root_account.account_authorization_configs.find(params[:id])
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def timeout_protection
|
||||
|
|
|
@ -34,7 +34,9 @@ class Login::OauthController < Login::OauthBaseController
|
|||
end
|
||||
|
||||
def create
|
||||
super
|
||||
reset_session_for_login
|
||||
|
||||
@aac = @domain_root_account.account_authorization_configs.find(params[:id])
|
||||
raise ActiveRecord::RecordNotFound unless @aac.is_a?(AccountAuthorizationConfig::Oauth)
|
||||
|
||||
oauth_state = session.delete(:oauth)
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
#
|
||||
# Copyright (C) 2015 Instructure, Inc.
|
||||
#
|
||||
# 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 Login
|
||||
OpenidConnectController = Oauth2Controller
|
||||
end
|
|
@ -7,7 +7,7 @@ module Login::Shared
|
|||
:enrollment,
|
||||
:expected_user_id,
|
||||
:masquerade_return_to,
|
||||
:oauth2_state)
|
||||
:oauth2_nonce)
|
||||
end
|
||||
|
||||
def successful_login(user, pseudonym, otp_passed = false)
|
||||
|
|
|
@ -41,17 +41,23 @@ class AccountAuthorizationConfig < ActiveRecord::Base
|
|||
case type_name
|
||||
when 'cas', 'ldap', 'saml'
|
||||
const_get(type_name.upcase)
|
||||
when 'facebook', 'twitter'
|
||||
when 'facebook', 'google', 'twitter'
|
||||
const_get(type_name.classify)
|
||||
when 'github'
|
||||
GitHub
|
||||
when 'linkedin'
|
||||
LinkedIn
|
||||
when 'openid_connect'
|
||||
OpenIDConnect
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
def self.sti_name
|
||||
display_name.underscore
|
||||
end
|
||||
|
||||
def self.singleton?
|
||||
false
|
||||
end
|
||||
|
@ -60,6 +66,10 @@ class AccountAuthorizationConfig < ActiveRecord::Base
|
|||
true
|
||||
end
|
||||
|
||||
def self.display_name
|
||||
name.demodulize
|
||||
end
|
||||
|
||||
belongs_to :account
|
||||
acts_as_list scope: :account
|
||||
|
||||
|
@ -73,7 +83,7 @@ class AccountAuthorizationConfig < ActiveRecord::Base
|
|||
:client_id, :client_secret, :domain,
|
||||
:consumer_key, :consumer_secret
|
||||
|
||||
VALID_AUTH_TYPES = %w[cas facebook github ldap linkedin saml twitter].freeze
|
||||
VALID_AUTH_TYPES = %w[cas facebook github google ldap linkedin openid_connect saml twitter].freeze
|
||||
validates_inclusion_of :auth_type, in: VALID_AUTH_TYPES, message: "invalid auth_type, must be one of #{VALID_AUTH_TYPES.join(',')}"
|
||||
validates_presence_of :account_id
|
||||
|
||||
|
@ -119,5 +129,6 @@ end
|
|||
|
||||
# so it doesn't get mixed up with ::CAS, ::LinkedIn and ::Twitter
|
||||
require_dependency 'account_authorization_config/cas'
|
||||
require_dependency 'account_authorization_config/google'
|
||||
require_dependency 'account_authorization_config/linked_in'
|
||||
require_dependency 'account_authorization_config/twitter'
|
||||
|
|
|
@ -21,10 +21,6 @@ class AccountAuthorizationConfig::Facebook < AccountAuthorizationConfig::Oauth2
|
|||
true
|
||||
end
|
||||
|
||||
def self.sti_name
|
||||
'facebook'
|
||||
end
|
||||
|
||||
def self.recognized_params
|
||||
if globally_configured?
|
||||
[ :auth_type ].freeze
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
#
|
||||
# Copyright (C) 2015 Instructure, Inc.
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
|
||||
class AccountAuthorizationConfig::Google < AccountAuthorizationConfig::OpenIDConnect
|
||||
def self.singleton?
|
||||
true
|
||||
end
|
||||
|
||||
def self.recognized_params
|
||||
if globally_configured?
|
||||
[ :auth_type ]
|
||||
else
|
||||
[ :auth_type, :client_id, :client_secret ].freeze
|
||||
end
|
||||
end
|
||||
|
||||
def self.globally_configured?
|
||||
Canvas::Plugin.find(:google_drive).enabled?
|
||||
end
|
||||
|
||||
def client_id
|
||||
self.class.globally_configured? ? settings[:client_id] : super
|
||||
end
|
||||
|
||||
def client_secret
|
||||
if self.class.globally_configured?
|
||||
settings[:client_secret_dec]
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def settings
|
||||
Canvas::Plugin.find(:google_drive).settings
|
||||
end
|
||||
|
||||
def client_options
|
||||
{
|
||||
site: 'https://accounts.google.com'.freeze,
|
||||
authorize_url: '/o/oauth2/auth'.freeze,
|
||||
token_url: '/o/oauth2/token'.freeze
|
||||
}
|
||||
end
|
||||
end
|
|
@ -45,7 +45,7 @@ class AccountAuthorizationConfig::LinkedIn < AccountAuthorizationConfig::Oauth2
|
|||
if self.class.globally_configured?
|
||||
settings[:client_secret_dec]
|
||||
else
|
||||
auth_decrypted_password
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -23,22 +23,28 @@ class AccountAuthorizationConfig::Oauth2 < AccountAuthorizationConfig::Delegated
|
|||
SENSITIVE_PARAMS = [ :client_secret ].freeze
|
||||
|
||||
# rename DB fields to something that makes sense for OAuth2
|
||||
def client_id=(val)
|
||||
self.entity_id = val
|
||||
end
|
||||
|
||||
def client_id
|
||||
entity_id
|
||||
end
|
||||
|
||||
alias_method :client_secret=, :auth_password=
|
||||
alias_method :client_secret, :auth_decrypted_password
|
||||
{ client_id: :entity_id,
|
||||
site: :auth_host,
|
||||
authorize_url: :log_in_url,
|
||||
token_url: :auth_base }.each do |(new_name, old_name)|
|
||||
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
||||
def #{new_name}=(val)
|
||||
self.#{old_name} = val
|
||||
end
|
||||
|
||||
def #{new_name}
|
||||
#{old_name}
|
||||
end
|
||||
RUBY
|
||||
end
|
||||
|
||||
def client
|
||||
@client ||= OAuth2::Client.new(client_id, client_secret, client_options)
|
||||
end
|
||||
|
||||
def authorize_url(redirect_uri, state)
|
||||
def generate_authorize_url(redirect_uri, state)
|
||||
client.auth_code.authorize_url({ redirect_uri: redirect_uri, state: state }.merge(authorize_options))
|
||||
end
|
||||
|
||||
|
@ -48,6 +54,14 @@ class AccountAuthorizationConfig::Oauth2 < AccountAuthorizationConfig::Delegated
|
|||
|
||||
protected
|
||||
|
||||
def client_options
|
||||
{
|
||||
site: site,
|
||||
authorize_url: authorize_url,
|
||||
token_url: token_url
|
||||
}
|
||||
end
|
||||
|
||||
def authorize_options
|
||||
{}
|
||||
end
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
#
|
||||
# Copyright (C) 2015 Instructure, Inc.
|
||||
#
|
||||
# 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/>.
|
||||
#
|
||||
|
||||
class AccountAuthorizationConfig::OpenIDConnect < AccountAuthorizationConfig::Oauth2
|
||||
def self.sti_name
|
||||
self == OpenIDConnect ? 'openid_connect'.freeze : super
|
||||
end
|
||||
|
||||
def self.display_name
|
||||
self == OpenIDConnect ? 'OpenID Connect'.freeze : super
|
||||
end
|
||||
|
||||
def self.recognized_params
|
||||
[ :auth_type, :client_id, :client_secret, :authorize_url, :token_url ].freeze
|
||||
end
|
||||
|
||||
def unique_id(token)
|
||||
JWT.decode(token.params['id_token'], nil, false).first['sub']
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def authorize_options
|
||||
{ scope: 'openid' }
|
||||
end
|
||||
end
|
|
@ -21,10 +21,6 @@ class AccountAuthorizationConfig::Twitter < AccountAuthorizationConfig::Oauth
|
|||
true
|
||||
end
|
||||
|
||||
def self.sti_name
|
||||
'twitter'
|
||||
end
|
||||
|
||||
def self.recognized_params
|
||||
if globally_configured?
|
||||
[ :auth_type ].freeze
|
||||
|
|
|
@ -72,7 +72,7 @@ class AccountAuthorizationConfigsPresenter
|
|||
|
||||
def sso_options
|
||||
new_auth_types.map do |auth_type|
|
||||
[auth_type.name.sub(/^AccountAuthorizationConfig::/, ''), auth_type.sti_name]
|
||||
[auth_type.display_name, auth_type.sti_name]
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
<%= fields_for(:account_authorization_config, aac) do |f| %>
|
||||
<%= f.hidden_field :auth_type, value: 'google' %>
|
||||
<%= f.hidden_field :id %>
|
||||
<table class="formtable" style="margin-left: 20px;">
|
||||
<%= render partial: "form_header",
|
||||
locals: { auth_type: t("Google"), presenter: presenter, aac: aac } %>
|
||||
<% unless AccountAuthorizationConfig::Google.globally_configured? %>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>
|
||||
<%= mt(<<-TEXT, google_url: "https://console.developers.google.com/project", callback_url: oauth2_login_callback_url)
|
||||
You will ned to create a web application in the [Google Developer Console](%{google_url}).
|
||||
You should add %{callback_url} as a redirect URI.
|
||||
After you create your app, make a note of the Client ID and Client Secret, and enter them here.
|
||||
TEXT
|
||||
%>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="vertical-align: top; width: 200px;">
|
||||
<%= f.blabel :client_id, en: "Client ID" %>
|
||||
</td>
|
||||
<td style="vertical-align: top;" class="nobr">
|
||||
<%= f.text_field :client_id, class: "auth_form", style: "width: 300px;" %>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="vertical-align: top; width: 200px;">
|
||||
<%= f.blabel :client_secret, en: "Client Secret" %>
|
||||
</td>
|
||||
<td style=" vertical-align: top;" class="nobr">
|
||||
<%= f.text_field :client_secret, value: '', class: "auth_form" %>
|
||||
<% if aac.auth_crypted_password -%>
|
||||
<span class="auth_form" style="font-size: smaller;">
|
||||
<br/>
|
||||
<%= t("Leave blank to continue using the current client secret.") %>
|
||||
</span>
|
||||
<% end %>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
<%= render partial: "form_footer",
|
||||
locals: { presenter: presenter, account: account, aac: aac, f: f } %>
|
||||
</table>
|
||||
<% end %>
|
|
@ -0,0 +1,68 @@
|
|||
<%= fields_for(:account_authorization_config, aac) do |f| %>
|
||||
<%= f.hidden_field :auth_type, value: 'openid_connect' %>
|
||||
<%= f.hidden_field :id %>
|
||||
<table class="formtable" style="margin-left: 20px;">
|
||||
<%= render partial: "form_header",
|
||||
locals: { auth_type: 'OpenID Connect', presenter: presenter, aac: aac } %>
|
||||
<tr>
|
||||
<td></td>
|
||||
<td>
|
||||
<%= mt(<<-TEXT, callback_url: oauth2_login_callback_url)
|
||||
To configure an OpenID Connect provider you need the authorize and token urls, and
|
||||
obtain a Client ID and Client Secret.
|
||||
|
||||
If the provider can configure valid callback or redirect URIs, use %{callback_url}.
|
||||
TEXT
|
||||
%>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="vertical-align: top; width: 200px;">
|
||||
<%= f.blabel :client_id, en: "Client ID" %>
|
||||
</td>
|
||||
<td style="vertical-align: top;" class="nobr">
|
||||
<%= f.text_field :client_id, class: "auth_form", style: "width: 300px;" %>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="vertical-align: top; width: 200px;">
|
||||
<%= f.blabel :client_secret, en: "Client Secret" %>
|
||||
</td>
|
||||
<td style=" vertical-align: top;" class="nobr">
|
||||
<%= f.text_field :client_secret, value: '', class: "auth_form" %>
|
||||
<% if aac.auth_crypted_password -%>
|
||||
<span class="auth_form" style="font-size: smaller;">
|
||||
<br/>
|
||||
<%= t("Leave blank to continue using the current client secret.") %>
|
||||
</span>
|
||||
<% end %>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="vertical-align: top; width: 200px;">
|
||||
<%= f.blabel :site, en: "Site" %>
|
||||
</td>
|
||||
<td style="vertical-align: top;" class="nobr">
|
||||
<%= f.text_field :site, class: "auth_form", style: "width: 300px;" %>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="vertical-align: top; width: 200px;">
|
||||
<%= f.blabel :authorize_url, en: "Authorize URL" %>
|
||||
</td>
|
||||
<td style="vertical-align: top;" class="nobr">
|
||||
<%= f.text_field :authorize_url, class: "auth_form", style: "width: 300px;" %>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="vertical-align: top; width: 200px;">
|
||||
<%= f.blabel :token_url, en: "Token URL" %>
|
||||
</td>
|
||||
<td style="vertical-align: top;" class="nobr">
|
||||
<%= f.text_field :token_url, class: "auth_form", style: "width: 300px;" %>
|
||||
</td>
|
||||
</tr>
|
||||
<%= render partial: "form_footer",
|
||||
locals: { presenter: presenter, account: account, aac: aac, f: f } %>
|
||||
</table>
|
||||
<% end %>
|
|
@ -15,7 +15,7 @@
|
|||
<tr>
|
||||
<td><%= f.blabel :client_secret, :en => "Client Secret" %></td>
|
||||
<td>
|
||||
<%= f.text_field :client_secret %>
|
||||
<%= f.password_field :client_secret %>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
|
||||
GoogleDocs::DriveConnection.config = Proc.new do
|
||||
Canvas::Plugin.find(:google_drive).try(:settings) || ConfigFile.load('google_drive')
|
||||
settings = Canvas::Plugin.find(:google_drive).try(:settings)
|
||||
if settings
|
||||
settings = settings.dup
|
||||
settings[:client_secret] = settings[:client_secret_dec]
|
||||
end
|
||||
settings || ConfigFile.load('google_drive')
|
||||
end
|
||||
|
||||
GoogleDocs::Connection.config = Proc.new do
|
||||
|
|
|
@ -637,7 +637,10 @@ CanvasRails::Application.routes.draw do
|
|||
|
||||
get 'login/facebook' => 'login/facebook#new', as: :facebook_login
|
||||
get 'login/github' => 'login/github#new', as: :github_login
|
||||
get 'login/google' => 'login/google#new', as: :google_login
|
||||
get 'login/linkedin' => 'login/linkedin#new', as: :linkedin_login
|
||||
get 'login/openid_connect' => 'login/openid_connect#new'
|
||||
get 'login/openid_connect/:id' => 'login/openid_connect#new', as: :openid_connect_login
|
||||
get 'login/twitter' => 'login/twitter#new', as: :twitter_login
|
||||
|
||||
get 'login/otp' => 'login/otp#new', as: :otp_login
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
class EncryptGoogleDriveSettings < ActiveRecord::Migration
|
||||
tag :predeploy
|
||||
|
||||
def up
|
||||
PluginSetting.where(name: 'google_drive').each do |ps|
|
||||
# do a dance so that we don't delete the unencrypted copy yet
|
||||
ps.encrypt_settings
|
||||
ps.initialize_plugin_setting
|
||||
ps.settings[:client_secret] = ps.settings[:client_secret_dec]
|
||||
PluginSetting.where(id: ps).update_all(settings: ps.settings.to_yaml)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,8 @@
|
|||
class DropUnencryptedGoogleDriveSettings < ActiveRecord::Migration
|
||||
tag :postdeploy
|
||||
|
||||
def up
|
||||
# the encryption callback automatically drops the unencrypted version
|
||||
PluginSetting.where(name: 'google_drive').each(&:save!)
|
||||
end
|
||||
end
|
|
@ -94,16 +94,17 @@ Canvas::Plugin.register('google_docs', :collaborations, {
|
|||
:settings_partial => 'plugins/google_docs_settings',
|
||||
:validator => 'GoogleDocsValidator'
|
||||
})
|
||||
Canvas::Plugin.register('google_drive', nil, {
|
||||
:name => lambda{ t :name, 'Google Drive' },
|
||||
:description => lambda{ t :description, 'Google Drive file sharing' },
|
||||
:website => 'http://drive.google.com',
|
||||
:author => 'Instructure',
|
||||
:author_website => 'http://www.instructure.com',
|
||||
:version => '1.0.0',
|
||||
:settings_partial => 'plugins/google_drive_settings',
|
||||
:validator => 'GoogleDriveValidator'
|
||||
})
|
||||
Canvas::Plugin.register('google_drive', nil,
|
||||
name: -> { t :name, 'Google Drive' },
|
||||
description: -> { t :description, 'Google Drive file sharing' },
|
||||
website: 'http://drive.google.com',
|
||||
author: 'Instructure',
|
||||
author_website: 'http://www.instructure.com',
|
||||
version: '1.0.0',
|
||||
settings_partial: 'plugins/google_drive_settings',
|
||||
validator: 'GoogleDriveValidator',
|
||||
encrypted_settings: [:client_secret]
|
||||
)
|
||||
Canvas::Plugin.register('kaltura', nil, {
|
||||
:name => lambda{ t :name, 'Kaltura' },
|
||||
:description => lambda{ t :description, 'Kaltura video/audio recording and playback'},
|
||||
|
|
|
@ -197,6 +197,14 @@ describe ApplicationController do
|
|||
|
||||
end
|
||||
|
||||
let(:empty_client_secrets) do
|
||||
{
|
||||
client_id: nil,
|
||||
client_secret: nil,
|
||||
redirect_uri: nil
|
||||
}.with_indifferent_access
|
||||
end
|
||||
|
||||
it "uses @real_current_user first" do
|
||||
mock_real_current_user = mock()
|
||||
mock_current_user = mock()
|
||||
|
@ -204,7 +212,7 @@ describe ApplicationController do
|
|||
controller.instance_variable_set(:@current_user, mock_current_user)
|
||||
|
||||
Rails.cache.expects(:fetch).with(['google_drive_tokens', mock_real_current_user].cache_key).returns(["real_current_user_refresh_token", "real_current_user_access_token"])
|
||||
GoogleDrive::Client.expects(:create).with({},"real_current_user_refresh_token", "real_current_user_access_token")
|
||||
GoogleDrive::Client.expects(:create).with(empty_client_secrets,"real_current_user_refresh_token", "real_current_user_access_token")
|
||||
controller.send(:google_drive_user_client)
|
||||
end
|
||||
|
||||
|
@ -213,7 +221,7 @@ describe ApplicationController do
|
|||
controller.instance_variable_set(:@real_current_user, nil)
|
||||
controller.instance_variable_set(:@current_user, mock_current_user)
|
||||
Rails.cache.expects(:fetch).with(['google_drive_tokens', mock_current_user].cache_key).returns(["current_user_refresh_token", "current_user_access_token"])
|
||||
GoogleDrive::Client.expects(:create).with({},"current_user_refresh_token", "current_user_access_token")
|
||||
GoogleDrive::Client.expects(:create).with(empty_client_secrets,"current_user_refresh_token", "current_user_access_token")
|
||||
controller.send(:google_drive_user_client)
|
||||
end
|
||||
|
||||
|
@ -228,7 +236,7 @@ describe ApplicationController do
|
|||
service_mock.stubs(first: mock(token: "user_refresh_token", access_token: "user_access_token"))
|
||||
mock_user_services.expects(:where).with(service: "google_drive").returns(service_mock)
|
||||
|
||||
GoogleDrive::Client.expects(:create).with({}, "user_refresh_token", "user_access_token")
|
||||
GoogleDrive::Client.expects(:create).with(empty_client_secrets, "user_refresh_token", "user_access_token")
|
||||
|
||||
controller.send(:google_drive_user_client)
|
||||
end
|
||||
|
@ -239,7 +247,7 @@ describe ApplicationController do
|
|||
session[:oauth_gdrive_access_token] = "access_token"
|
||||
session[:oauth_gdrive_refresh_token] = "refresh_token"
|
||||
|
||||
GoogleDrive::Client.expects(:create).with({}, "refresh_token", "access_token")
|
||||
GoogleDrive::Client.expects(:create).with(empty_client_secrets, "refresh_token", "access_token")
|
||||
controller.send(:google_drive_user_client)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -30,40 +30,39 @@ describe Login::Oauth2Controller do
|
|||
get :new, auth_type: 'facebook'
|
||||
expect(response).to be_redirect
|
||||
expect(response.location).to match(%r{^https://www.facebook.com/dialog/oauth\?})
|
||||
expect(session[:oauth2_state]).to_not be_blank
|
||||
expect(session[:oauth2_nonce]).to_not be_blank
|
||||
end
|
||||
end
|
||||
|
||||
describe "#create" do
|
||||
it "checks the OAuth2 CSRF token" do
|
||||
session[:oauth2_state] = 'bob'
|
||||
get :create, id: aac
|
||||
# it could be a 422, or 0 if error handling isn't enabled properly in specs
|
||||
expect(response).to_not be_success
|
||||
expect(response).to_not be_redirect
|
||||
|
||||
get :create, id: aac, state: 'garbage'
|
||||
session[:oauth2_nonce] = 'bob'
|
||||
jwt = Canvas::Security.create_jwt(aac_id: aac.global_id, nonce: 'different')
|
||||
get :create, state: jwt
|
||||
# it could be a 422, or 0 if error handling isn't enabled properly in specs
|
||||
expect(response).to_not be_success
|
||||
expect(response).to_not be_redirect
|
||||
end
|
||||
|
||||
it "works" do
|
||||
session[:oauth2_state] = 'bob'
|
||||
session[:oauth2_nonce] = 'bob'
|
||||
aac.any_instantiation.expects(:get_token).returns('token')
|
||||
aac.any_instantiation.expects(:unique_id).with('token').returns('user')
|
||||
user_with_pseudonym(username: 'user', active_all: 1)
|
||||
|
||||
get :create, id: aac, state: 'bob'
|
||||
jwt = Canvas::Security.create_jwt(aac_id: aac.global_id, nonce: 'bob')
|
||||
get :create, state: jwt
|
||||
expect(response).to redirect_to(dashboard_url(login_success: 1))
|
||||
end
|
||||
|
||||
it "redirects to login if no user found" do
|
||||
aac.any_instantiation.expects(:get_token).returns('token')
|
||||
aac.any_instantiation.expects(:unique_id).with('token').returns('user')
|
||||
controller.expects(:check_csrf)
|
||||
|
||||
get :create, id: aac
|
||||
session[:oauth2_nonce] = 'bob'
|
||||
jwt = Canvas::Security.create_jwt(aac_id: aac.global_id, nonce: 'bob')
|
||||
|
||||
get :create, state: jwt
|
||||
expect(response).to redirect_to(login_url)
|
||||
expect(flash[:delegated_message]).to_not be_blank
|
||||
end
|
||||
|
|
|
@ -149,8 +149,10 @@ describe AccountAuthorizationConfigsPresenter do
|
|||
expect(presenter.sso_options).to eq([['CAS', 'cas'],
|
||||
['Facebook', 'facebook'],
|
||||
['GitHub', 'github'],
|
||||
['Google', 'google'],
|
||||
['LDAP', 'ldap'],
|
||||
['LinkedIn', 'linkedin'],
|
||||
['OpenID Connect', 'openid_connect'],
|
||||
['Twitter', 'twitter']])
|
||||
end
|
||||
|
||||
|
|
Loading…
Reference in New Issue