From 0cc3b2bd6290b17904d19a922b9ae681c20e3318 Mon Sep 17 00:00:00 2001
From: Bilal
Date: Wed, 12 Aug 2020 13:52:22 +0200
Subject: [PATCH] Get code and token from Azure AD and store it to the DB
---
Gemfile | 7 +-
Gemfile.lock | 9 +-
app/controllers/callbacks_controller.rb | 12 +-
app/views/profiles/show.html.erb | 2 +-
config/initializers/omniauth.rb | 29 ++---
config/routes.rb | 2 +-
...810140331_add_microsoft_tokens_to_users.rb | 7 +
db/structure.sql | 8 +-
lib/azure_ad.rb | 121 ++++++++++++++++++
9 files changed, 171 insertions(+), 26 deletions(-)
create mode 100644 db/migrate/20200810140331_add_microsoft_tokens_to_users.rb
create mode 100644 lib/azure_ad.rb
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