diff --git a/Gemfile b/Gemfile index 098be61..425a27b 100644 --- a/Gemfile +++ b/Gemfile @@ -139,9 +139,14 @@ gem 'rack-cors' # Ruby wrappers for the HubSpot REST API gem "hubspot-ruby" +# OAuth +gem 'omniauth-oauth2', '~> 1.6' +# OmniAuth CSRF protection +gem 'omniauth-rails_csrf_protection', '~> 0.1.2' + # authenticate via Microsoft # gem 'omniauth-microsoft_graph', git: 'https://github.com/m4c3/omniauth-microsoft_graph' -gem 'omniauth-microsoft_graph' +# gem 'omniauth-microsoft_graph' group :development, :test, :review do # Call "byebug" anywhere in the code to stop execution and get a debugger console diff --git a/Gemfile.lock b/Gemfile.lock index 15ae75b..34bba69 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -319,12 +319,12 @@ GEM omniauth (1.9.1) hashie (>= 3.4.6) rack (>= 1.6.2, < 3) - omniauth-microsoft_graph (0.3.3) - omniauth (~> 1.1, >= 1.1.1) - omniauth-oauth2 (~> 1.6) omniauth-oauth2 (1.6.0) oauth2 (~> 1.1) omniauth (~> 1.9) + omniauth-rails_csrf_protection (0.1.2) + actionpack (>= 4.2) + omniauth (>= 1.3.1) parallel (1.19.1) parity (3.2.0) parser (2.6.5.0) @@ -569,7 +569,8 @@ DEPENDENCIES mux_ruby! oath (~> 1.1.0) oath-generators (~> 1.0.1) - omniauth-microsoft_graph + omniauth-oauth2 (~> 1.6) + omniauth-rails_csrf_protection (~> 0.1.2) parity (~> 3.2.0) pdf-reader (~> 2.1.0) pdfkit (~> 0.8.2) diff --git a/app/controllers/callbacks_controller.rb b/app/controllers/callbacks_controller.rb index 2f727ae..b3c4989 100644 --- a/app/controllers/callbacks_controller.rb +++ b/app/controllers/callbacks_controller.rb @@ -5,6 +5,16 @@ class CallbacksController < ApplicationController skip_before_action :verify_authenticity_token def create - render plain: params.inspect + token_data = request.env['omniauth.auth'][:credentials] + + current_user&.tap do |user| + user.microsoft_access_token = token_data.token + user.microsoft_refresh_token = token_data.refresh_token + user.microsoft_token_expires_at = token_data.expires_at # Expiration time is returned in seconds + user.save + end + + redirect_to profile_path end + end diff --git a/app/views/profiles/show.html.erb b/app/views/profiles/show.html.erb index 2f4775b..d677cab 100644 --- a/app/views/profiles/show.html.erb +++ b/app/views/profiles/show.html.erb @@ -17,7 +17,7 @@ <%= @user.role_for(Current.account).to_s.titleize %> <% end %>

- <%= link_to 'Auth to Microsoft', '/auth/microsoft_graph', class: "btn btn-primary" %> + <%= link_to 'Auth to Microsoft', '/auth/azure_ad', method: :post, class: "btn btn-primary" %>
diff --git a/config/initializers/omniauth.rb b/config/initializers/omniauth.rb index d85c6e7..07e6abc 100644 --- a/config/initializers/omniauth.rb +++ b/config/initializers/omniauth.rb @@ -1,20 +1,17 @@ -ENV['AZURE_CLIENT_ID'] = 'c45b93ae-ef07-415d-b13a-ab566b877c1c' -ENV['AZURE_CLIENT_SECRET'] = 'XVboF2sRaS_H2oK6I9R56.A_exnRhiv~Xt' -ENV['AZURE_TENANT_ID'] = '1e33d1c7-dfb4-4df1-86da-62770313bcb0' -ENV['AZURE_EXTENSIONS'] = '' - -# Rails.application.config.middleware.use OmniAuth::Builder do -# provider :microsoft_graph,{ -# client_id: ENV['AZURE_CLIENT_ID'], -# client_secret: ENV['AZURE_CLIENT_SECRET'], -# tenant_id: ENV['AZURE_TENANT_ID'], -# extensions: ENV['AZURE_EXTENSIONS'], -# redirect_uri: 'https://517e57c6cd6c.ngrok.io/auth/microsoft_graph/callback', -# scope: 'openid email profile User.Read' -# } -# end +require 'azure_ad' Rails.application.config.middleware.use OmniAuth::Builder do - provider :microsoft_graph, ENV['AZURE_CLIENT_ID'], ENV['AZURE_CLIENT_SECRET'], scope: 'openid email profile User.Read' + provider :azure_ad, + client_id: ENV['AZURE_CLIENT_ID'], + client_secret: ENV['AZURE_CLIENT_SECRET'], + redirect_uri: ENV['AZURE_REDIRECT_URI'], + client_options: { + token_url: "#{ENV['AZURE_TENANT_ID']}/oauth2/v2.0/token", + authorize_url: "#{ENV['AZURE_TENANT_ID']}/oauth2/v2.0/authorize" + }, + scope: ENV['AZURE_SCOPES'] end +# Rails.application.config.middleware.use OmniAuth::Builder do +# provider :microsoft_graph, ENV['AZURE_CLIENT_ID'], ENV['AZURE_CLIENT_SECRET'], scope: 'openid email profile User.Read' +# end \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index e18fa00..32bf489 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -4,7 +4,7 @@ require 'sidekiq/web' Rails.application.routes.draw do AVAILABLE_LOCALES_REGEX = /#{I18n.available_locales.join("|")}/.freeze - get 'auth/microsoft_graph/callback', to: 'callbacks#create' + get 'auth/azure_ad/callback', to: 'callbacks#create' concern :confirmable do resources :video_release_confirmations, only: [:new, :create, :destroy] diff --git a/db/migrate/20200810140331_add_microsoft_tokens_to_users.rb b/db/migrate/20200810140331_add_microsoft_tokens_to_users.rb new file mode 100644 index 0000000..c58be13 --- /dev/null +++ b/db/migrate/20200810140331_add_microsoft_tokens_to_users.rb @@ -0,0 +1,7 @@ +class AddMicrosoftTokensToUsers < ActiveRecord::Migration[6.0] + def change + add_column :users, :microsoft_access_token, :string + add_column :users, :microsoft_refresh_token, :string + add_column :users, :microsoft_token_expires_at, :integer + end +end \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index fce0e15..d5a9301 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -1755,7 +1755,10 @@ CREATE TABLE public.users ( remember_created_at timestamp without time zone, first_name character varying, last_name character varying, - time_zone character varying DEFAULT 'UTC'::character varying NOT NULL + time_zone character varying DEFAULT 'UTC'::character varying NOT NULL, + microsoft_access_token character varying, + microsoft_refresh_token character varying, + microsoft_token_expires_at integer ); @@ -3966,6 +3969,7 @@ INSERT INTO "schema_migrations" (version) VALUES ('20200720051634'), ('20200720131309'), ('20200721140821'), -('20200725231419'); +('20200725231419'), +('20200810140331'); diff --git a/lib/azure_ad.rb b/lib/azure_ad.rb new file mode 100644 index 0000000..1899dd0 --- /dev/null +++ b/lib/azure_ad.rb @@ -0,0 +1,121 @@ +require 'omniauth-oauth2' + +module OmniAuth + module Strategies + class AzureAd < OmniAuth::Strategies::OAuth2 + BASE_SCOPE_URL = 'https://graph.microsoft.com/' + BASE_SCOPES = %w[offline_access openid email profile].freeze + DEFAULT_SCOPE = 'offline_access openid email profile User.Read'.freeze + + option :name, :azure_ad + + option :client_options, + site: 'https://login.microsoftonline.com/' + + option :authorize_options, %i[state callback_url access_type auth_type scope prompt login_hint domain_hint response_mode] + + option :token_params, {} + + option :scope, DEFAULT_SCOPE + option :authorized_client_ids, [] + + uid { raw_info["id"] } + + info do + { + # 'email' => raw_info["mail"], + # 'first_name' => raw_info["givenName"], + # 'last_name' => raw_info["surname"], + # 'name' => [raw_info["givenName"], raw_info["surname"]].join(' '), + # 'nickname' => raw_info["displayName"], + } + end + + extra do + { + # 'raw_info' => raw_info, + # 'params' => access_token.params, + # 'aud' => options.client_id + } + end + + def authorize_params + super.tap do |params| + options[:authorize_options].each do |k| + params[k] = request.params[k.to_s] unless [nil, ''].include?(request.params[k.to_s]) + end + + params[:scope] = get_scope(params) + + session['omniauth.state'] = params[:state] if params[:state] + end + end + + def raw_info + @raw_info ||= access_token.get('https://graph.microsoft.com/v1.0/me').parsed + end + + def callback_url + options[:callback_url] || full_host + script_name + callback_path + end + + def custom_build_access_token + token_response = get_access_token(request) + session[:microsoft_graph_api_token] = token_response.token + token_response + end + + alias build_access_token custom_build_access_token + + private + + def get_access_token(request) + verifier = request.params['code'] + redirect_uri = request.params['redirect_uri'] || request.params['callback_url'] + if verifier && request.xhr? + client_get_token(verifier, redirect_uri || '/auth/azure_ad/callback') + elsif verifier + client_get_token(verifier, redirect_uri || callback_url) + elsif verify_token(request.params['access_token']) + ::OAuth2::AccessToken.from_hash(client, request.params.dup) + elsif request.content_type =~ /json/i + begin + body = JSON.parse(request.body.read) + request.body.rewind # rewind request body for downstream middlewares + verifier = body && body['code'] + client_get_token(verifier, '/auth/azure_ad/callback') if verifier + rescue JSON::ParserError => e + warn "[omniauth google-oauth2] JSON parse error=#{e}" + end + end + end + + def client_get_token(verifier, redirect_uri) + client.auth_code.get_token(verifier, get_token_options(redirect_uri), get_token_params) + end + + def get_token_params + deep_symbolize(options.auth_token_params || {}) + end + + def get_token_options(redirect_uri = '') + { redirect_uri: redirect_uri }.merge(token_params.to_hash(symbolize_keys: true)) + end + + def get_scope(params) + raw_scope = params[:scope] || DEFAULT_SCOPE + scope_list = raw_scope.split(' ').map { |item| item.split(',') }.flatten + scope_list.map! { |s| s =~ %r{^https?://} || BASE_SCOPES.include?(s) ? s : "#{BASE_SCOPE_URL}#{s}" } + scope_list.join(' ') + end + + def verify_token(access_token) + return false unless access_token + # access_token.get('https://graph.microsoft.com/v1.0/me').parsed + raw_response = client.request(:get, 'https://graph.microsoft.com/v1.0/me', + params: { access_token: access_token }).parsed + (raw_response['aud'] == options.client_id) || options.authorized_client_ids.include?(raw_response['aud']) + end + end + end +end \ No newline at end of file