diff --git a/app/assets/javascripts/app/views/microsoft365/app_config.jst.eco b/app/assets/javascripts/app/views/microsoft365/app_config.jst.eco
index fbf0ebe91..8bc671671 100644
--- a/app/assets/javascripts/app/views/microsoft365/app_config.jst.eco
+++ b/app/assets/javascripts/app/views/microsoft365/app_config.jst.eco
@@ -20,6 +20,14 @@
+
diff --git a/db/migrate/20210426184355_issue_3446_microsoft_365_tenants.rb b/db/migrate/20210426184355_issue_3446_microsoft_365_tenants.rb
new file mode 100644
index 000000000..9bb17fedd
--- /dev/null
+++ b/db/migrate/20210426184355_issue_3446_microsoft_365_tenants.rb
@@ -0,0 +1,16 @@
+class Issue3446Microsoft365Tenants < ActiveRecord::Migration[5.2]
+ def up
+ return if !Setting.exists?(name: 'system_init_done')
+
+ setting = Setting.find_by name: 'auth_microsoft_office365_credentials'
+ setting.options[:form].push({
+ display: 'App Tenant ID',
+ null: true,
+ name: 'app_tenant',
+ tag: 'input',
+ placeholder: 'common',
+ })
+
+ setting.save!
+ end
+end
diff --git a/db/seeds/settings.rb b/db/seeds/settings.rb
index cc46e45bf..d5af58d19 100644
--- a/db/seeds/settings.rb
+++ b/db/seeds/settings.rb
@@ -1563,6 +1563,13 @@ Setting.create_if_not_exists(
name: 'app_secret',
tag: 'input',
},
+ {
+ display: 'App Tenant ID',
+ null: true,
+ name: 'app_tenant',
+ tag: 'input',
+ placeholder: 'common',
+ },
],
},
state: {},
diff --git a/lib/external_credential/microsoft365.rb b/lib/external_credential/microsoft365.rb
index 869b52399..f95652d29 100644
--- a/lib/external_credential/microsoft365.rb
+++ b/lib/external_credential/microsoft365.rb
@@ -16,12 +16,16 @@ class ExternalCredential::Microsoft365
if credentials[:client_secret].blank?
credentials[:client_secret] = external_credential.credentials['client_secret']
end
+ # client_tenant may be empty. Set only if key is nonexistant at all
+ if !credentials.key? :client_tenant
+ credentials[:client_tenant] = external_credential.credentials['client_tenant']
+ end
end
raise Exceptions::UnprocessableEntity, 'No client_id param!' if credentials[:client_id].blank?
raise Exceptions::UnprocessableEntity, 'No client_secret param!' if credentials[:client_secret].blank?
- authorize_url = generate_authorize_url(credentials[:client_id])
+ authorize_url = generate_authorize_url(credentials)
{
authorize_url: authorize_url,
@@ -33,7 +37,7 @@ class ExternalCredential::Microsoft365
raise Exceptions::UnprocessableEntity, 'No Microsoft365 app configured!' if !external_credential
raise Exceptions::UnprocessableEntity, 'No code for session found!' if !params[:code]
- response = authorize_tokens(external_credential.credentials[:client_id], external_credential.credentials[:client_secret], params[:code])
+ response = authorize_tokens(external_credential.credentials, params[:code])
%w[refresh_token access_token expires_in scope token_type id_token].each do |key|
raise Exceptions::UnprocessableEntity, "No #{key} for authorization request found!" if response[key.to_sym].blank?
end
@@ -66,6 +70,7 @@ class ExternalCredential::Microsoft365
type: 'XOAUTH2',
client_id: external_credential.credentials[:client_id],
client_secret: external_credential.credentials[:client_secret],
+ client_tenant: external_credential.credentials[:client_tenant],
),
}
@@ -156,10 +161,9 @@ class ExternalCredential::Microsoft365
channel
end
- def self.generate_authorize_url(client_id, scope = 'https://outlook.office.com/IMAP.AccessAsUser.All https://outlook.office.com/SMTP.Send offline_access openid profile email')
-
+ def self.generate_authorize_url(credentials, scope = 'https://outlook.office.com/IMAP.AccessAsUser.All https://outlook.office.com/SMTP.Send offline_access openid profile email')
params = {
- 'client_id' => client_id,
+ 'client_id' => credentials[:client_id],
'redirect_uri' => ExternalCredential.callback_url('microsoft365'),
'scope' => scope,
'response_type' => 'code',
@@ -167,28 +171,20 @@ class ExternalCredential::Microsoft365
'prompt' => 'consent',
}
+ tenant = credentials[:client_tenant].presence || 'common'
+
uri = URI::HTTPS.build(
host: 'login.microsoftonline.com',
- path: '/common/oauth2/v2.0/authorize',
+ path: "/#{tenant}/oauth2/v2.0/authorize",
query: params.to_query
)
uri.to_s
end
- def self.authorize_tokens(client_id, client_secret, authorization_code)
- params = {
- 'client_secret' => client_secret,
- 'code' => authorization_code,
- 'grant_type' => 'authorization_code',
- 'client_id' => client_id,
- 'redirect_uri' => ExternalCredential.callback_url('microsoft365'),
- }
-
- uri = URI::HTTPS.build(
- host: 'login.microsoftonline.com',
- path: '/common/oauth2/v2.0/token',
- )
+ def self.authorize_tokens(credentials, authorization_code)
+ uri = authorize_tokens_uri(credentials[:client_tenant])
+ params = authorize_tokens_params(credentials, authorization_code)
response = Net::HTTP.post_form(uri, params)
if response.code != 200 && response.body.blank?
@@ -207,19 +203,28 @@ class ExternalCredential::Microsoft365
result.symbolize_keys
end
+ def self.authorize_tokens_params(credentials, authorization_code)
+ {
+ 'client_secret' => credentials[:client_secret],
+ 'code' => authorization_code,
+ 'grant_type' => 'authorization_code',
+ 'client_id' => credentials[:client_id],
+ 'redirect_uri' => ExternalCredential.callback_url('microsoft365'),
+ }
+ end
+
+ def self.authorize_tokens_uri(tenant)
+ URI::HTTPS.build(
+ host: 'login.microsoftonline.com',
+ path: "/#{tenant.presence || 'common'}/oauth2/v2.0/token",
+ )
+ end
+
def self.refresh_token(token)
return token if token[:created_at] >= Time.zone.now - 50.minutes
- params = {
- 'client_id' => token[:client_id],
- 'client_secret' => token[:client_secret],
- 'refresh_token' => token[:refresh_token],
- 'grant_type' => 'refresh_token',
- }
- uri = URI::HTTPS.build(
- host: 'login.microsoftonline.com',
- path: '/common/oauth2/v2.0/token',
- )
+ params = refresh_token_params(token)
+ uri = refresh_token_uri(token)
response = Net::HTTP.post_form(uri, params)
if response.code != 200 && response.body.blank?
@@ -238,6 +243,24 @@ class ExternalCredential::Microsoft365
)
end
+ def self.refresh_token_params(credentials)
+ {
+ 'client_id' => credentials[:client_id],
+ 'client_secret' => credentials[:client_secret],
+ 'refresh_credentials' => credentials[:refresh_credentials],
+ 'grant_type' => 'refresh_credentials',
+ }
+ end
+
+ def self.refresh_token_uri(credentials)
+ tenant = credentials[:client_tenant].presence || 'common'
+
+ URI::HTTPS.build(
+ host: 'login.microsoftonline.com',
+ path: "/#{tenant}/oauth2/v2.0/token",
+ )
+ end
+
def self.user_info(id_token)
split = id_token.split('.')[1]
return if split.blank?
diff --git a/lib/omniauth/microsoft_office365_database.rb b/lib/omniauth/microsoft_office365_database.rb
index 5d51c084a..e3ec00a21 100644
--- a/lib/omniauth/microsoft_office365_database.rb
+++ b/lib/omniauth/microsoft_office365_database.rb
@@ -7,7 +7,12 @@ class MicrosoftOffice365Database < OmniAuth::Strategies::MicrosoftOffice365
config = Setting.get('auth_microsoft_office365_credentials') || {}
args[0] = config['app_id']
args[1] = config['app_secret']
+ tenant = config['app_tenant'].presence || 'common'
+
super
+
+ @options[:client_options][:authorize_url] = "/#{tenant}/oauth2/v2.0/authorize"
+ @options[:client_options][:token_url] = "/#{tenant}/oauth2/v2.0/token"
end
end
diff --git a/spec/db/migrate/issue_3446_microsoft_365_tenants_spec.rb b/spec/db/migrate/issue_3446_microsoft_365_tenants_spec.rb
new file mode 100644
index 000000000..ee5a609a5
--- /dev/null
+++ b/spec/db/migrate/issue_3446_microsoft_365_tenants_spec.rb
@@ -0,0 +1,25 @@
+require 'rails_helper'
+
+RSpec.describe Issue3446Microsoft365Tenants, type: :db_migration do
+ context 'when having pre-tenant setting' do
+ before do
+ setting.options['form'] = setting.options['form'].slice 0, 2
+ setting.save!
+ end
+
+ let(:setting) { Setting.find_by(name: 'auth_microsoft_office365_credentials') }
+
+ it 'adds tenant field to form options' do
+ expect { migrate }
+ .to change { setting.reload.options['form'].last['name'] }
+ .to('app_tenant')
+ end
+
+ it 'changes form fields count from 2 to 3 ' do
+ expect { migrate }
+ .to change { setting.reload.options['form'].count }
+ .from(2)
+ .to(3)
+ end
+ end
+end
diff --git a/spec/lib/external_credential/microsoft365_spec.rb b/spec/lib/external_credential/microsoft365_spec.rb
index afd49f1bc..4c41e2bd4 100644
--- a/spec/lib/external_credential/microsoft365_spec.rb
+++ b/spec/lib/external_credential/microsoft365_spec.rb
@@ -3,7 +3,9 @@ require 'rails_helper'
RSpec.describe ExternalCredential::Microsoft365 do
let(:token_url) { 'https://login.microsoftonline.com/common/oauth2/v2.0/token' }
+ let(:token_url_with_tenant) { 'https://login.microsoftonline.com/tenant/oauth2/v2.0/token' }
let(:authorize_url) { "https://login.microsoftonline.com/common/oauth2/v2.0/authorize?access_type=offline&client_id=#{client_id}&prompt=consent&redirect_uri=http%3A%2F%2Fzammad.example.com%2Fapi%2Fv1%2Fexternal_credentials%2Fmicrosoft365%2Fcallback&response_type=code&scope=https%3A%2F%2Foutlook.office.com%2FIMAP.AccessAsUser.All+https%3A%2F%2Foutlook.office.com%2FSMTP.Send+offline_access+openid+profile+email" }
+ let(:authorize_url_with_tenant) { "https://login.microsoftonline.com/tenant/oauth2/v2.0/authorize?access_type=offline&client_id=#{client_id}&prompt=consent&redirect_uri=http%3A%2F%2Fzammad.example.com%2Fapi%2Fv1%2Fexternal_credentials%2Fmicrosoft365%2Fcallback&response_type=code&scope=https%3A%2F%2Foutlook.office.com%2FIMAP.AccessAsUser.All+https%3A%2F%2Foutlook.office.com%2FSMTP.Send+offline_access+openid+profile+email" }
let(:id_token) { 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6ImtnMkxZczJUMENUaklmajRydDZKSXluZW4zOCJ9.eyJhdWQiOiIyMTk4NTFhYS0wMDAwLTRhNDctMTExMS0zMmQwNzAyZTAxMjM0IiwiaXNzIjoiaHR0cHM6Ly9sb2dpbi5taWNyb3NvZnRvbmxpbmUuY29tLzM2YTlhYjU1LWZpZmEtMjAyMC04YTc4LTkwcnM0NTRkYmNmZDJkL3YyLjAiLCJpYXQiOjEzMDE1NTE4MzUsIm5iZiI6MTMwMTU1MTgzNSwiZXhwIjoxNjAxNTU5NzQ0LCJuYW1lIjoiRXhhbXBsZSBVc2VyIiwib2lkIjoiMTExYWIyMTQtMTJzNy00M2NnLThiMTItM2ozM2UydDBjYXUyIiwicHJlZmVycmVkX3VzZXJuYW1lIjoidGVzdEBleGFtcGxlLmNvbSIsImVtYWlsIjoidGVzdEBleGFtcGxlLmNvbSIsInJoIjoiMC40MjM0LWZmZnNmZGdkaGRLZUpEU1hiejlMYXBSbUNHZGdmZ2RmZ0kwZHkwSEF1QlhaSEFNYy4iLCJzdWIiOiJYY0VlcmVyQkVnX0EzNWJlc2ZkczNMTElXNjU1NFQtUy0ycGRnZ2R1Z3c1NDNXT2xJIiwidGlkIjoiMzZhOWFiNTUtZmlmYS0yMDIwLThhNzgtOTByczQ1NGRiY2ZkMmQiLCJ1dGkiOiJEU0dGZ3Nhc2RkZmdqdGpyMzV3cWVlIiwidmVyIjoiMi4wIn0=.l0nglq4rIlkR29DFK3PQFQTjE-VeHdgLmcnXwGvT8Z-QBaQjeTAcoMrVpr0WdL6SRYiyn2YuqPnxey6N0IQdlmvTMBv0X_dng_y4CiQ8ABdZrQK0VSRWZViboJgW5iBvJYFcMmVoilHChueCzTBnS1Wp2KhirS2ymUkPHS6AB98K0tzOEYciR2eJsJ2JOdo-82oOW4w6tbbqMvzT3DzsxqPQRGe2hUbNqo6gcwJLqq4t0bNf5XiYThw1sv4IivERmqW_pfybXEseKyZGd4NnJ6WwwOgTz5tkoLwls_YeDZVcp_Fpw9XR7J0UlyPqLtoUEjVihdyrJjAbdtHFKdOjrw' }
let(:access_token) { '000.0000lvC3gAbjs8CYoKitfqM5LBS5N13374MCg6pNpZ28mxO2HuZvg0000_rsW00aACmFEto1BJeGDuu0000vmV6Esqv78iec-FbEe842ZevQtOOemQyQXjhMs62K1E6g3ehDLPRp6j4vtpSKSb6I-3MuDPfdzdqI23hM0' }
@@ -15,6 +17,7 @@ RSpec.describe ExternalCredential::Microsoft365 do
let(:client_id) { '123' }
let(:client_secret) { '345' }
+ let(:client_tenant) { 'tenant' }
let(:authorization_code) { '567' }
let(:email_address) { 'test@example.com' }
@@ -329,9 +332,14 @@ RSpec.describe ExternalCredential::Microsoft365 do
describe '.generate_authorize_url' do
it 'generates valid URL' do
- url = described_class.generate_authorize_url(client_id)
+ url = described_class.generate_authorize_url(client_id: client_id)
expect(url).to eq(authorize_url)
end
+
+ it 'generates valid URL with tenant' do
+ url = described_class.generate_authorize_url(client_id: client_id, client_tenant: 'tenant')
+ expect(url).to eq(authorize_url_with_tenant)
+ end
end
describe '.user_info' do