HOT FROM THE OVEN

How to Implement Sign In With Microsoft Using the Office 365 Omniauth Strategy With Rails 7

How to Implement Sign In With Microsoft Using the Office 365 Omniauth Strategy With Rails 7

Whilst scrolling through the world wide web, have you ever encountered buttons with titles such as “Sign in with ‘X' service”? Well, those are there for a reason, and their purpose is to make your, and the developer’s life easier. Much easier. Let me explain why.

Back in the early days of web development, in order for a user to sign in and create a user session, they had to authorize themselves via their credentials - mainly their email and password. All users, and their corresponding credentials, had been stored on a backend server, which was used to compare form given credentials in order to authorize the user and grant them a session. That way, a user was authorized and could proceed to access the website. However, due to safety reasons and maintenance issues, using this kind of user authorization scheme has been proven to be bad practice nowadays.

One big concern during those times had been exposing user credentials to external services. Before the appearance of OAuth, in order to use an external service, a user had to provide their credentials to that service, which in turn would allow the service to perform certain actions on the user’s behalf.

Think of it this way - you’re a user, and the year is 2007. You’ve signed up to a new website, but now that website wants you to share your thoughts and experiences with people outside of that website’s domain. In this case, it wants to use an external service like Gmail. The website is asking you for your Gmail address and password so it can log into your account, and send emails to all of your contacts. Afterward, the website promises to forget your password and never use it again. Would you trust such a claim?

Yeah, neither would I. OAuth was developed to avoid such security risks. Generally, all OAuth does is redirect you to the domain of the external service upon which you are asked to authorize yourself. In short; instead of allowing a random website to obtain your user information for an external service, you are authorizing yourself via that external service using their domain. Short, simple, and safe, just how we like it here. On another note, as proof of successful user authorization, OAuth provides you with an access token which in turn allows access to the website’s API service.

OAuth

How to Easily Set up an External Authorization Procedure

This blog post is going to demonstrate how you can implement the “Sign in Microsoft” button with its full functionality by using Ruby 3.0.0 and Rails 7.0.2. Setting up an external Microsoft authorization prompt uses the same OAuth approach as explained above. In this case, Omniauth is the gem that will be used for multi-provider OAuth authentication. To implement it, we have to use the Microsoft Office OmniAuth strategy (gem). The following is everything needed for the task:

gem 'devise"', '~> 4.8'
gem 'omniauth', '~> 2.0.4'
gem 'omniauth-microsoft-office365'
gem 'dotenv-rails', groups: [:development, :test]
gem 'omniauth-rails_csrf_protection'

Adding the CSRF protection gem is necessary for being able to mitigate issues with Cross-Site Request Forgery on the request phase when using the OmniAuth gem with a Rails application.

The next main thing is registering a new Microsoft application from which we can get a Client ID and Client Secret. That can be done in the Microsoft Azure portal.

Before returning to the application side, make sure to set the homepage URL and callback URL on the Azure portal side. The homepage URL is the default server that is used when you run rails server which in this case is: http://localhost:3000. The callback URL is where the office strategy is directed after the authentication process whether or not the user passes the authentication. In our case, it will be set to: http://localhost:3000/auth/microsoft_office365/callback

The Client ID (Application ID), and the Client Secret (Application Secret) are used to validate and recognize requests that are coming from your application. They’re linked to the application side, and as such, will be saved as environment variables (this is why the 'dotenv-rails' is being used):

OFFICE365_KEY ="< your_application_id_goes_here >"
OFFICE365_SECRET = "< your_client_secret_goes_here >"

On the other note, now that the authorization server has been set up, we have to set up our application to accept requests and redirect towards the Microsoft OAuth strategy. For user authentication on the application’s side, we use Devise, whose setup is well explained on the official Github page.

Since Devise is not initially fully compatible with Turbo (which is built into Rails 7 by default), there are some adjustments needed to the Devise initializer config. The primary reason for that Turbo allows for asynchronous page updates without writing any javascript. This behavior blocks Devise from displaying flash messages which are a core part of Devise itself. The following configuration re-establishes the default Devise behavior by adding a custom Turbo class. Credit goes where it’s due, and one can easily follow a guide on how to use Devise with Hotwire & Turbo.js.

First, an update on how the error messages are being handled is required:

# app/controllers/turbo_controller.rb

class TurboController < ApplicationController
class Responder < ActionController::Responder
def to_turbo_stream
controller.render(options.merge(formats: :html))
rescue ActionView::MissingTemplate => error
if get?
raise error
elsif has_errors? && default_action
render rendering_options.merge(formats: :html, status: :unprocessable_entity)
else
redirect_to navigation_location
end
end
end

self.responder = Responder
respond_to :html, :turbo_stream
end

Afterward, the following have to be added to the devise initializer.

# config/initializers/devise.rb

class TurboFailureApp < Devise::FailureApp
def respond
if request_format == :turbo_stream
redirect
else
super
end
end

def skip_format?
%w(html turbo_stream */*).include? request_format.to_s
end
end

Devise.setup do |config|
# ==> Controller configuration
config.parent_controller = 'TurboController'

# ==> Navigation configuration
config.navigational_formats = [ '*/*', :html, :turbo_stream]

# ==> OmniAuth
config.omniauth :microsoft_office365, ENV['OFFICE365_KEY'], ENV['OFFICE365_SECRET'], callback_path: "/auth/microsoft_office365/callback"

config.warden do |manager|
manager.failure_app = TurboFailureApp
end
end

Now, Devise has been properly initialized to fully work with Turbo and Rails 7. The authorization application has been set up as well with the proper callback URL. All that is left now is to add the button and the logic behind it on a real-world example.

Since we’re doing user authentication and authorization, we need users to begin with. Devise offers a great tool for generating everything we need for that: rails g devise user. Now, after that, we'll need two columns added to your users' table so that a user can hold a 'uid' and 'provider'. These we'll be populated once the office Omniauth strategy has successfully authenticated a user.

To do so, make migration with: rails g migration add_oauth_support_to_users and add the following lines:

add_column(:users, :provider, :string, limit: 50, null: false, default:' ')
add_column(:users, :uid, :string, limit: 50, null: false, default:' ')

Run rails db:migrate.

In the User model, update the devise part of the code to look like this:

devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable,
:omniauthable, omniauth_providers: [:microsoft_office365]

In the same file, we'll add a new method to deal with the callback data that the strategy will provide:

def self.find_for_oauth(provider_data)
user = User.where(provider: provider_data.provider, uid: provider_data.uid).first

unless user
user = User.create(
uid: provider_data.uid,
provider: provider_data.provider,
email: provider_data.info.email,
password: Devise. friendly_token[0, 20]
)
end

user
end

For the last step, we need to create a route for this. Since we’re doing with Devise, the routes have already been implemented, but we need to intercept them in order to add the Omniauth part of the logic. In the ‘routes.rb’ file, make sure to add the following:

devise_scope :user do
get "users", to: "devise/sessions#new"
get '/auth/microsoft_office365/callback', to: 'omniauth_callbacks#microsoft_office365'
end
devise_for :users, controllers: { omniauth_callbacks: 'omniauth_callbacks' }

For Devise to be able to understand additional logic happening during the authentication process, we need to add our own controller which looks like the following:

# app/controller/omniauth_callbacks_controller.rb

class OmniauthCallbacksController < Devise::OmniauthCallbacksController
def microsoft_office365
callback_from :microsoft_office365
end

def failure
redirect_to after_omniauth_failure_path_for(resource_name)
end

private

def callback_from(provider)
@user = User.find_for_oauth(request.env['omniauth.auth'])

if @user.persisted?
sign_in_and_redirect @user
else
flash[:error] = 'There was a problem signing you in through Office 365. Please register or try signing in later.'
redirect_to new_user_registration_url
end
end
end

This flow creates a new user and saves the information in an instance variable ‘@user’. Next, if the user persisted in the external validation process, they’re signed in, and if not, they’re redirected to the registration page. After all of this, that should be it, you should be ready to use your own ‘Sign in with Office365’ button.

sign-in

Debugging

If for whatever reason Omniauth decides not to cooperate with your project when dealing with raised exceptions after unsuccessful login attempts, try adding the following initializer, and it may aid the setting up process:

# config/initializers/fix_omniauth.rb

begin
require "omniauth"
require "omniauth/version"
rescue LoadError => e
warn "Could not load 'omniauth'. Please ensure you have the omniauth gem >= 1.0.0 installed and listed in your Gemfile."
raise
end

# Clean up the default path_prefix. It will be automatically set by Devise.
OmniAuth.config.path_prefix = nil

OmniAuth.config.on_failure = Proc.new do |env|
env['devise.mapping'] = Devise.mappings[:user]
controller_name = ActiveSupport::Inflector.camelize(env['devise.mapping'].controllers[:omniauth_callbacks])
controller_klass = ActiveSupport::Inflector.constantize("#{controller_name}Controller")
controller_klass.action(:failure).call(env)
end

module Devise
module OmniAuth
autoload :Config, "devise/omniauth/config"
autoload :UrlHelpers, "devise/omniauth/url_helpers"
end
end

Wrap Up: How to Create a Fully Working “Sign in With Microsoft” Authorization Button

In order to be able to implement your own “Sign in with Microsoft” authorization button, you would need to do the following:

  • Include the Omniauth gem in your project (along with the omniauth office strategy, and other dependencies explained at the beginning of this post)
  • Setup your application via the Microsoft Azure portal
  • Retrieve your Application Key & Application Secret from the Azure portal, and save them on the Application side (preferably using a .env file)
  • Add required error message handling updates
  • Add required Devise initializer changes
  • Add database changes, configure callback methods, edit routes, add a custom controller for handling authorizations

There you go! We’ve started with nothing and ended up with a fully working “Sign in with Microsoft” authorization button, which functions exactly the same way as the ones you’ve seen everywhere online!

Now that you know this, you may have no issues implementing other Omniauth strategies on your own. The premise stays the same:

  • Find the omniauth strategy you want to implement
  • Create a server side application using the strategy provider (i.e. Google, Facebook, Reddit…)
  • Implement the authorization part on the application side

And you’re done - simple as that!

I’d personally advise trying to implement the Google authorization strategy next as it seems to be the simplest one to do. Give it a go, and I hope you make it through the challenge.