Fixes #3515 - Allow single tenant only application in microsoft

This commit is contained in:
Boris Manojlovic 2021-03-08 14:13:15 +01:00 committed by Thorsten Eckel
parent 2001ac6968
commit 85d6e19892
7 changed files with 122 additions and 30 deletions

View file

@ -20,6 +20,14 @@
<input id="client_secret" type="text" name="client_secret" value="<% if @external_credential && @external_credential.credentials: %><%= @external_credential.credentials.client_secret %><% end %>" class="form-control" required autocomplete="off" > <input id="client_secret" type="text" name="client_secret" value="<% if @external_credential && @external_credential.credentials: %><%= @external_credential.credentials.client_secret %><% end %>" class="form-control" required autocomplete="off" >
</div> </div>
</div> </div>
<div class="input form-group">
<div class="formGroup-label">
<label for="client_tenant">Tenant UUID/Name</label>
</div>
<div class="controls">
<input id="client_tenant" type="text" name="client_tenant" value="<%= @external_credential?.credentials?.client_tenant %>" class="form-control" required autocomplete="off" placeholder="common">
</div>
</div>
<h2><%- @T('Your callback URL') %></h2> <h2><%- @T('Your callback URL') %></h2>
<div class="input form-group"> <div class="input form-group">
<div class="controls"> <div class="controls">

View file

@ -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

View file

@ -1563,6 +1563,13 @@ Setting.create_if_not_exists(
name: 'app_secret', name: 'app_secret',
tag: 'input', tag: 'input',
}, },
{
display: 'App Tenant ID',
null: true,
name: 'app_tenant',
tag: 'input',
placeholder: 'common',
},
], ],
}, },
state: {}, state: {},

View file

@ -16,12 +16,16 @@ class ExternalCredential::Microsoft365
if credentials[:client_secret].blank? if credentials[:client_secret].blank?
credentials[:client_secret] = external_credential.credentials['client_secret'] credentials[:client_secret] = external_credential.credentials['client_secret']
end 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 end
raise Exceptions::UnprocessableEntity, 'No client_id param!' if credentials[:client_id].blank? raise Exceptions::UnprocessableEntity, 'No client_id param!' if credentials[:client_id].blank?
raise Exceptions::UnprocessableEntity, 'No client_secret param!' if credentials[:client_secret].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, 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 Microsoft365 app configured!' if !external_credential
raise Exceptions::UnprocessableEntity, 'No code for session found!' if !params[:code] 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| %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? raise Exceptions::UnprocessableEntity, "No #{key} for authorization request found!" if response[key.to_sym].blank?
end end
@ -66,6 +70,7 @@ class ExternalCredential::Microsoft365
type: 'XOAUTH2', type: 'XOAUTH2',
client_id: external_credential.credentials[:client_id], client_id: external_credential.credentials[:client_id],
client_secret: external_credential.credentials[:client_secret], client_secret: external_credential.credentials[:client_secret],
client_tenant: external_credential.credentials[:client_tenant],
), ),
} }
@ -156,10 +161,9 @@ class ExternalCredential::Microsoft365
channel channel
end 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 = { params = {
'client_id' => client_id, 'client_id' => credentials[:client_id],
'redirect_uri' => ExternalCredential.callback_url('microsoft365'), 'redirect_uri' => ExternalCredential.callback_url('microsoft365'),
'scope' => scope, 'scope' => scope,
'response_type' => 'code', 'response_type' => 'code',
@ -167,28 +171,20 @@ class ExternalCredential::Microsoft365
'prompt' => 'consent', 'prompt' => 'consent',
} }
tenant = credentials[:client_tenant].presence || 'common'
uri = URI::HTTPS.build( uri = URI::HTTPS.build(
host: 'login.microsoftonline.com', host: 'login.microsoftonline.com',
path: '/common/oauth2/v2.0/authorize', path: "/#{tenant}/oauth2/v2.0/authorize",
query: params.to_query query: params.to_query
) )
uri.to_s uri.to_s
end end
def self.authorize_tokens(client_id, client_secret, authorization_code) def self.authorize_tokens(credentials, authorization_code)
params = { uri = authorize_tokens_uri(credentials[:client_tenant])
'client_secret' => client_secret, params = authorize_tokens_params(credentials, authorization_code)
'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',
)
response = Net::HTTP.post_form(uri, params) response = Net::HTTP.post_form(uri, params)
if response.code != 200 && response.body.blank? if response.code != 200 && response.body.blank?
@ -207,19 +203,28 @@ class ExternalCredential::Microsoft365
result.symbolize_keys result.symbolize_keys
end 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) def self.refresh_token(token)
return token if token[:created_at] >= Time.zone.now - 50.minutes return token if token[:created_at] >= Time.zone.now - 50.minutes
params = { params = refresh_token_params(token)
'client_id' => token[:client_id], uri = refresh_token_uri(token)
'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',
)
response = Net::HTTP.post_form(uri, params) response = Net::HTTP.post_form(uri, params)
if response.code != 200 && response.body.blank? if response.code != 200 && response.body.blank?
@ -238,6 +243,24 @@ class ExternalCredential::Microsoft365
) )
end 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) def self.user_info(id_token)
split = id_token.split('.')[1] split = id_token.split('.')[1]
return if split.blank? return if split.blank?

View file

@ -7,7 +7,12 @@ class MicrosoftOffice365Database < OmniAuth::Strategies::MicrosoftOffice365
config = Setting.get('auth_microsoft_office365_credentials') || {} config = Setting.get('auth_microsoft_office365_credentials') || {}
args[0] = config['app_id'] args[0] = config['app_id']
args[1] = config['app_secret'] args[1] = config['app_secret']
tenant = config['app_tenant'].presence || 'common'
super super
@options[:client_options][:authorize_url] = "/#{tenant}/oauth2/v2.0/authorize"
@options[:client_options][:token_url] = "/#{tenant}/oauth2/v2.0/token"
end end
end end

View file

@ -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

View file

@ -3,7 +3,9 @@ require 'rails_helper'
RSpec.describe ExternalCredential::Microsoft365 do RSpec.describe ExternalCredential::Microsoft365 do
let(:token_url) { 'https://login.microsoftonline.com/common/oauth2/v2.0/token' } 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) { "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(:id_token) { 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6ImtnMkxZczJUMENUaklmajRydDZKSXluZW4zOCJ9.eyJhdWQiOiIyMTk4NTFhYS0wMDAwLTRhNDctMTExMS0zMmQwNzAyZTAxMjM0IiwiaXNzIjoiaHR0cHM6Ly9sb2dpbi5taWNyb3NvZnRvbmxpbmUuY29tLzM2YTlhYjU1LWZpZmEtMjAyMC04YTc4LTkwcnM0NTRkYmNmZDJkL3YyLjAiLCJpYXQiOjEzMDE1NTE4MzUsIm5iZiI6MTMwMTU1MTgzNSwiZXhwIjoxNjAxNTU5NzQ0LCJuYW1lIjoiRXhhbXBsZSBVc2VyIiwib2lkIjoiMTExYWIyMTQtMTJzNy00M2NnLThiMTItM2ozM2UydDBjYXUyIiwicHJlZmVycmVkX3VzZXJuYW1lIjoidGVzdEBleGFtcGxlLmNvbSIsImVtYWlsIjoidGVzdEBleGFtcGxlLmNvbSIsInJoIjoiMC40MjM0LWZmZnNmZGdkaGRLZUpEU1hiejlMYXBSbUNHZGdmZ2RmZ0kwZHkwSEF1QlhaSEFNYy4iLCJzdWIiOiJYY0VlcmVyQkVnX0EzNWJlc2ZkczNMTElXNjU1NFQtUy0ycGRnZ2R1Z3c1NDNXT2xJIiwidGlkIjoiMzZhOWFiNTUtZmlmYS0yMDIwLThhNzgtOTByczQ1NGRiY2ZkMmQiLCJ1dGkiOiJEU0dGZ3Nhc2RkZmdqdGpyMzV3cWVlIiwidmVyIjoiMi4wIn0=.l0nglq4rIlkR29DFK3PQFQTjE-VeHdgLmcnXwGvT8Z-QBaQjeTAcoMrVpr0WdL6SRYiyn2YuqPnxey6N0IQdlmvTMBv0X_dng_y4CiQ8ABdZrQK0VSRWZViboJgW5iBvJYFcMmVoilHChueCzTBnS1Wp2KhirS2ymUkPHS6AB98K0tzOEYciR2eJsJ2JOdo-82oOW4w6tbbqMvzT3DzsxqPQRGe2hUbNqo6gcwJLqq4t0bNf5XiYThw1sv4IivERmqW_pfybXEseKyZGd4NnJ6WwwOgTz5tkoLwls_YeDZVcp_Fpw9XR7J0UlyPqLtoUEjVihdyrJjAbdtHFKdOjrw' }
let(:access_token) { '000.0000lvC3gAbjs8CYoKitfqM5LBS5N13374MCg6pNpZ28mxO2HuZvg0000_rsW00aACmFEto1BJeGDuu0000vmV6Esqv78iec-FbEe842ZevQtOOemQyQXjhMs62K1E6g3ehDLPRp6j4vtpSKSb6I-3MuDPfdzdqI23hM0' } let(:access_token) { '000.0000lvC3gAbjs8CYoKitfqM5LBS5N13374MCg6pNpZ28mxO2HuZvg0000_rsW00aACmFEto1BJeGDuu0000vmV6Esqv78iec-FbEe842ZevQtOOemQyQXjhMs62K1E6g3ehDLPRp6j4vtpSKSb6I-3MuDPfdzdqI23hM0' }
@ -15,6 +17,7 @@ RSpec.describe ExternalCredential::Microsoft365 do
let(:client_id) { '123' } let(:client_id) { '123' }
let(:client_secret) { '345' } let(:client_secret) { '345' }
let(:client_tenant) { 'tenant' }
let(:authorization_code) { '567' } let(:authorization_code) { '567' }
let(:email_address) { 'test@example.com' } let(:email_address) { 'test@example.com' }
@ -329,9 +332,14 @@ RSpec.describe ExternalCredential::Microsoft365 do
describe '.generate_authorize_url' do describe '.generate_authorize_url' do
it 'generates valid 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) expect(url).to eq(authorize_url)
end 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 end
describe '.user_info' do describe '.user_info' do