From 64c70c2e72cd293c5e6dc1af4b49b35bc081c836 Mon Sep 17 00:00:00 2001 From: Nicolas Marlier Date: Mon, 11 May 2020 12:10:32 +0200 Subject: [PATCH] Support for Oauth2 v2.0 --- lib/omniauth/azure_oauth2.rb | 3 +- lib/omniauth/azure_oauth2/version.rb | 2 +- lib/omniauth/strategies/azure_oauth2_v2.rb | 67 ++++ .../strategies/azure_oauth2_v2_spec.rb | 330 ++++++++++++++++++ 4 files changed, 400 insertions(+), 2 deletions(-) create mode 100644 lib/omniauth/strategies/azure_oauth2_v2.rb create mode 100644 spec/omniauth/strategies/azure_oauth2_v2_spec.rb diff --git a/lib/omniauth/azure_oauth2.rb b/lib/omniauth/azure_oauth2.rb index 69651ed..2d8b756 100644 --- a/lib/omniauth/azure_oauth2.rb +++ b/lib/omniauth/azure_oauth2.rb @@ -1 +1,2 @@ -require File.join('omniauth', 'strategies', 'azure_oauth2') \ No newline at end of file +require File.join('omniauth', 'strategies', 'azure_oauth2') +require File.join('omniauth', 'strategies', 'azure_oauth2_v2') diff --git a/lib/omniauth/azure_oauth2/version.rb b/lib/omniauth/azure_oauth2/version.rb index cfaa9dd..037a6ad 100644 --- a/lib/omniauth/azure_oauth2/version.rb +++ b/lib/omniauth/azure_oauth2/version.rb @@ -1,5 +1,5 @@ module OmniAuth module AzureOauth2 - VERSION = "0.0.10" + VERSION = "0.1.0" end end diff --git a/lib/omniauth/strategies/azure_oauth2_v2.rb b/lib/omniauth/strategies/azure_oauth2_v2.rb new file mode 100644 index 0000000..955c972 --- /dev/null +++ b/lib/omniauth/strategies/azure_oauth2_v2.rb @@ -0,0 +1,67 @@ +require 'omniauth/strategies/oauth2' +require 'jwt' + +module OmniAuth + module Strategies + class AzureOauth2V2 < OmniAuth::Strategies::OAuth2 + BASE_AZURE_URL = 'https://login.microsoftonline.com' + + option :name, 'azure_oauth2_v2' + option :tenant_provider, nil + + DEFAULT_SCOPE = 'openid profile email' + USER_INFO_URL = 'https://graph.microsoft.com/v1.0/me' + + # tenant_provider must return client_id, client_secret and optionally tenant_id and base_azure_url + args [:tenant_provider] + + def client + if options.tenant_provider + provider = options.tenant_provider.new(self) + else + provider = options # if pass has to config, get mapped right on to options + end + + options.client_id = provider.client_id + options.client_secret = provider.client_secret + options.tenant_id = + provider.respond_to?(:tenant_id) ? provider.tenant_id : 'common' + options.base_azure_url = + provider.respond_to?(:base_azure_url) ? provider.base_azure_url : BASE_AZURE_URL + + options.authorize_params = provider.authorize_params if provider.respond_to?(:authorize_params) + options.authorize_params.domain_hint = provider.domain_hint if provider.respond_to?(:domain_hint) && provider.domain_hint + options.authorize_params.prompt = request.params['prompt'] if defined? request && request.params['prompt'] + options.authorize_params.scope = (provider.scope if provider.respond_to?(:scope) && provider.scope) || DEFAULT_SCOPE + + options.client_options.authorize_url = "#{options.base_azure_url}/#{options.tenant_id}/oauth2/v2.0/authorize" + options.client_options.token_url = "#{options.base_azure_url}/#{options.tenant_id}/oauth2/v2.0/token" + + super + end + + uid { + raw_info['id'] + } + + info do + { + name: raw_info['displayName'], + first_name: raw_info['givenName'], + last_name: raw_info['surname'], + email: raw_info['userPrincipalName'], + id: raw_info['id'], + } + end + + def callback_url + full_host + script_name + callback_path + end + + def raw_info + @raw_info ||= access_token.get(USER_INFO_URL).parsed + end + + end + end +end diff --git a/spec/omniauth/strategies/azure_oauth2_v2_spec.rb b/spec/omniauth/strategies/azure_oauth2_v2_spec.rb new file mode 100644 index 0000000..7612d05 --- /dev/null +++ b/spec/omniauth/strategies/azure_oauth2_v2_spec.rb @@ -0,0 +1,330 @@ +require 'spec_helper' +require 'omniauth-azure-oauth2' + +module OmniAuth + module Strategies + module JWT; end + end +end + +describe OmniAuth::Strategies::AzureOauth2V2 do + let(:request) { double('Request', :params => {}, :cookies => {}, :env => {}) } + let(:app) { + lambda do + [200, {}, ["Hello."]] + end + } + + before do + OmniAuth.config.test_mode = true + end + + after do + OmniAuth.config.test_mode = false + end + + describe 'static configuration' do + let(:options) { @options || {} } + subject do + OmniAuth::Strategies::AzureOauth2V2.new(app, {client_id: 'id', client_secret: 'secret', tenant_id: 'tenant'}.merge(options)) + end + + describe '#client' do + it 'has correct authorize url' do + allow(subject).to receive(:request) { request } + expect(subject.client.options[:authorize_url]).to eql('https://login.microsoftonline.com/tenant/oauth2/v2.0/authorize') + end + + it 'has correct authorize params' do + allow(subject).to receive(:request) { request } + subject.client + expect(subject.authorize_params[:domain_hint]).to be_nil + end + + it 'has correct token url' do + allow(subject).to receive(:request) { request } + expect(subject.client.options[:token_url]).to eql('https://login.microsoftonline.com/tenant/oauth2/v2.0/token') + end + + describe "overrides" do + it 'should override domain_hint' do + @options = {domain_hint: 'hint'} + allow(subject).to receive(:request) { request } + subject.client + expect(subject.authorize_params[:domain_hint]).to eql('hint') + end + end + end + + end + + describe 'static configuration - german' do + let(:options) { @options || {} } + subject do + OmniAuth::Strategies::AzureOauth2V2.new(app, {client_id: 'id', client_secret: 'secret', tenant_id: 'tenant', base_azure_url: 'https://login.microsoftonline.de'}.merge(options)) + end + + describe '#client' do + it 'has correct authorize url' do + allow(subject).to receive(:request) { request } + expect(subject.client.options[:authorize_url]).to eql('https://login.microsoftonline.de/tenant/oauth2/v2.0/authorize') + end + + it 'has correct authorize params' do + allow(subject).to receive(:request) { request } + subject.client + expect(subject.authorize_params[:domain_hint]).to be_nil + end + + it 'has correct token url' do + allow(subject).to receive(:request) { request } + expect(subject.client.options[:token_url]).to eql('https://login.microsoftonline.de/tenant/oauth2/v2.0/token') + end + + it 'has correct authorize_params' do + allow(subject).to receive(:request) { request } + subject.client + expect(subject.authorize_params[:scope]).to eql('openid profile email') + end + + describe "overrides" do + it 'should override domain_hint' do + @options = {domain_hint: 'hint'} + allow(subject).to receive(:request) { request } + subject.client + expect(subject.authorize_params[:domain_hint]).to eql('hint') + end + end + end + end + + describe 'static common configuration' do + let(:options) { @options || {} } + subject do + OmniAuth::Strategies::AzureOauth2V2.new(app, {client_id: 'id', client_secret: 'secret'}.merge(options)) + end + + before do + allow(subject).to receive(:request) { request } + end + + describe '#client' do + it 'has correct authorize url' do + expect(subject.client.options[:authorize_url]).to eql('https://login.microsoftonline.com/common/oauth2/v2.0/authorize') + end + + it 'has correct token url' do + expect(subject.client.options[:token_url]).to eql('https://login.microsoftonline.com/common/oauth2/v2.0/token') + end + end + end + + describe 'dynamic configuration' do + let(:provider_klass) { + Class.new { + def initialize(strategy) + end + + def client_id + 'id' + end + + def client_secret + 'secret' + end + + def tenant_id + 'tenant' + end + + def authorize_params + { custom_option: 'value' } + end + } + } + + subject do + OmniAuth::Strategies::AzureOauth2V2.new(app, provider_klass) + end + + before do + allow(subject).to receive(:request) { request } + end + + describe '#client' do + it 'has correct authorize url' do + expect(subject.client.options[:authorize_url]).to eql('https://login.microsoftonline.com/tenant/oauth2/v2.0/authorize') + end + + it 'has correct authorize params' do + subject.client + expect(subject.authorize_params[:domain_hint]).to be_nil + expect(subject.authorize_params[:custom_option]).to eql('value') + end + + it 'has correct token url' do + expect(subject.client.options[:token_url]).to eql('https://login.microsoftonline.com/tenant/oauth2/v2.0/token') + end + + it 'has correct authorize_params' do + subject.client + expect(subject.authorize_params[:scope]).to eql('openid profile email') + end + + # todo: how to get this working? + # describe "overrides" do + # it 'should override domain_hint' do + # provider_klass.domain_hint = 'hint' + # subject.client + # expect(subject.authorize_params[:domain_hint]).to eql('hint') + # end + # end + end + + end + + describe 'dynamic configuration - german' do + let(:provider_klass) { + Class.new { + def initialize(strategy) + end + + def client_id + 'id' + end + + def client_secret + 'secret' + end + + def tenant_id + 'tenant' + end + + def base_azure_url + 'https://login.microsoftonline.de' + end + + def scope + 'Calendars.ReadWrite email offline_access User.Read' + end + } + } + + subject do + OmniAuth::Strategies::AzureOauth2V2.new(app, provider_klass) + end + + before do + allow(subject).to receive(:request) { request } + end + + describe '#client' do + it 'has correct authorize url' do + expect(subject.client.options[:authorize_url]).to eql('https://login.microsoftonline.de/tenant/oauth2/v2.0/authorize') + end + + it 'has correct authorize params' do + subject.client + expect(subject.authorize_params[:domain_hint]).to be_nil + end + + it 'has correct token url' do + expect(subject.client.options[:token_url]).to eql('https://login.microsoftonline.de/tenant/oauth2/v2.0/token') + end + + it 'has correct scope' do + subject.client + expect(subject.authorize_params[:scope]).to eql('Calendars.ReadWrite email offline_access User.Read') + end + + # todo: how to get this working? + # describe "overrides" do + # it 'should override domain_hint' do + # provider_klass.domain_hint = 'hint' + # subject.client + # expect(subject.authorize_params[:domain_hint]).to eql('hint') + # end + # end + end + + end + + describe 'dynamic common configuration' do + let(:provider_klass) { + Class.new { + def initialize(strategy) + end + + def client_id + 'id' + end + + def client_secret + 'secret' + end + } + } + + subject do + OmniAuth::Strategies::AzureOauth2V2.new(app, provider_klass) + end + + before do + allow(subject).to receive(:request) { request } + end + + describe '#client' do + it 'has correct authorize url' do + expect(subject.client.options[:authorize_url]).to eql('https://login.microsoftonline.com/common/oauth2/v2.0/authorize') + end + + it 'has correct token url' do + expect(subject.client.options[:token_url]).to eql('https://login.microsoftonline.com/common/oauth2/v2.0/token') + end + end + end + + describe "raw_info" do + subject do + OmniAuth::Strategies::AzureOauth2V2.new(app, {client_id: 'id', client_secret: 'secret'}) + end + + let(:access_token) do + double + end + + before do + allow(subject).to receive(:access_token) { access_token } + allow(subject).to receive(:request) { request } + expect(access_token) + .to receive(:get) + .with('https://graph.microsoft.com/v1.0/me') + .and_return( + double({ + parsed: { + 'id' => 'my_id', + 'displayName' => 'Bob Doe', + 'givenName' => 'Bob', + 'surname' => 'Doe', + 'userPrincipalName' => 'bob@doe.com' + } + }) + ) + end + + it "info returns correct info" do + expect(subject.info).to eq({ + email: 'bob@doe.com', + first_name: 'Bob', + id: 'my_id', + last_name: 'Doe', + name: 'Bob Doe', + }) + end + + it "uid returns correct uid" do + expect(subject.uid).to eq('my_id') + end + end +end