Fixes #3515 - Allow single tenant only application in microsoft
This commit is contained in:
parent
2001ac6968
commit
85d6e19892
7 changed files with 122 additions and 30 deletions
|
@ -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">
|
||||||
|
|
|
@ -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
|
|
@ -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: {},
|
||||||
|
|
|
@ -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?
|
||||||
|
|
|
@ -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
|
||||||
|
|
25
spec/db/migrate/issue_3446_microsoft_365_tenants_spec.rb
Normal file
25
spec/db/migrate/issue_3446_microsoft_365_tenants_spec.rb
Normal 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
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue