skip to Main Content

I need to have a user login to their Azure Active Directory account from an iOS app and, after acquiring a token, passing the information on to my rails server which would then authenticate the user and create their local (Rails) user record if the authentication succeeds. Or, in the case of an existing user, it would (ideally) set up the Devise current_user variable so that subsequent requests for resources on the Rails app would succeed.

I have both sides of this equation working independently–the iOS/Swift 4 application uses the Microsoft SDK to acquire the token–which works correctly. And the rails side is set up with the Azure Active Directory Omniauth provider and it works correctly when you access it through a browser. So far so good. (Note: both are using the Azure AD v1.0 APIs: ADAL on iOS and omniauth-azure-activedirectory on Rails)

When I’ve worked with Omniauth in the past–specifically using the Facebook provider, I could just acquire the token on the app side and pass it down to the Rails server which would then use the Facebook graph to make sure the token was legit with a simple GET request. Using the Microsoft Omniauth provider, however, there doesn’t seem to be an equivalent. I am finding that the tokens and claims match up between the two, but I’m at a loss as to the best approach for indicating to rails that the user is legit and should be instantiated as the current_user in Devise if the authentication details match. In other words, what field(s) should I send to the rails side that I can then pass along to the Azure AD to verify the user’s authenticity?

Should I try to use the Omniauth provider and give it back what the web-based version provides (e.g. omniauth.auth hash) or should I write my own controller/action that receives the auth details from the app and instantiate the user myself after having validated the user again from Rails? I’ve started down the path on both approaches but nothing has worked so far.

Am I on the right path with either of those approaches or are there any other suggestions?

Update

I have switched now to using the v2.0 APIs and I’ve set up a new application (at apps.dev.microsoft.com) that uses MSAL on the app side and microsoft_v2_auth on the rails side. Once I get an access token back from MSAL on the app side, I send it to my rails server in the body of a POST (over HTTPS). Server side, I just do a GET against Graph API ‘me’ endpoint in a separate action (unrelated to the Omniauth provider) using the token I got from the app as a bearer token in the authorization header. If this comes back without an error I create the user if they don’t exist and then sign them in or just sign them in if they do exist. This seems to work.

I am just using the Omniauth provider for web logins.

So! Does this approach seem sound? I really don’t need anything from Azure AD except to authenticate users. A GET request from the Graph API using a token I got from the app seems like the simplest way to do this? Any other thoughts or concerns?

2

Answers


  1. Chosen as BEST ANSWER

    Since I've been asked for some deeper insight into this, I'll go ahead and post the solution I ended up implementing.

    Add this to your gemfile:

    gem 'omniauth-microsoft_v2_auth'
    

    Add this to the bottom of your config/devise.rb file:

    config.omniauth :microsoft_v2_auth, ENV['CLIENT_ID'], ENV['CLIENT_SECRET']
    

    You'll have to add those environment variables to your environment. I typically use the Figaro gem while in development and then set them via Heroku CLI when deployed.

    You'll need to route users to your Azure authentication via sessions controller. Add this to your routes.rb file:

    devise_for :users, :controllers => {sessions: 'sessions', registrations: 'registrations', omniauth_callbacks: "callbacks"} 
    
    devise_scope :user do
      post 'users/sign_in_with_azad_token' => 'sessions#sign_in_with_azad_token'
    end
    

    Here's how I implement the action in my sessions controller:

    def sign_in_with_azad_token
        token = params[:token]
    
        # Use the graph API to ensure this user is legit
        uri = URI("https://graph.microsoft.com/v1.0/me/")
        request = Net::HTTP::Get.new(uri.request_uri)
        request['authorization'] = "Bearer #{token}"
    
        response = Net::HTTP.start(uri.host, uri.port, :use_ssl => uri.scheme == 'https') do |http|
            http.request(request)
        end
    
        json = JSON.parse(response.body)
    
        if json["error"].nil?
            # If no error, user was found
            user = User.from_uid_email(json["id"], json["userPrincipalName"])
            unless user.nil?
                sign_in("user", user)
                respond_to do |format|  
                    format.json {  return render :json => {  :success => true, :user => user } }  
                end
            else
                respond_to do |format|  
                    format.json {  return render :json => {  :success => false, :message => "You must sign in before continuing" }, status: :unauthorized }  
                end
            end
    
        else
            respond_to do |format|  
                format.json {  return render :json => {  :success => false, :message => json["error"]["message"]}, status: :unauthorized }  
            end
        end
    
    end
    

    You may want to have a way to allow your users to authenticate using Microsoft Azure in the webi browser. In that case you'll need to add this to your Devise new registrations view (mine is located in views/devise/registrations/new.html.erb and views/devise/shared/_links.html.erb):

        <%- if devise_mapping.omniauthable? %>
          <%- resource_class.omniauth_providers.each do |provider| %>
            <%= link_to "Sign in with #{OmniAuth::Utils.camelize(provider)}", omniauth_authorize_path(resource_name, provider) %><br />
          <% end -%>
        <% end -%>
    

    Finally, the endpoint where my client app performs its login is at ...[server_url]/users/sign_in_with_azad_token.json

    I hit this endpoint after I've authenticated using the MSAL iOS framework on the app side and obtained the token. I include the token in a JSON payload that is part of the request body. The payload is simply:

    {
        "token" : "dj9hs0shgkshd93hs92bh7493"
    
    }
    

    Hope this helps. It's been a while since I've worked on this, so I may have forgotten something. Add a comment to this answer if anything seems missing.


  2. Take a look at https://github.com/joshsoftware/sso-devise-omniauth-client for ideas. SSO is supported by Devise, but you’ll need to set up some controllers and probably a custom OmniAuth strategy.

    Also maybe checkout http://codetheory.in/rails-devise-omniauth-sso/

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search