{"id":2168,"date":"2015-09-11T15:51:09","date_gmt":"2015-09-11T10:21:09","guid":{"rendered":"http:\/\/codetheory.in\/?p=2168"},"modified":"2015-09-23T21:43:51","modified_gmt":"2015-09-23T16:13:51","slug":"rails-devise-omniauth-sso","status":"publish","type":"post","link":"https:\/\/codetheory.in\/rails-devise-omniauth-sso\/","title":{"rendered":"Single Sign On (SSO) for Multiple Applications with Devise, OmniAuth and Custom OAuth2 Implementation in Rails"},"content":{"rendered":"
Recently I had to implement Single Sign On<\/a> (SSO) for one of the Rails app I’d been working on. Since Devise is already fairly popular to integrate an authentication system in Rails app, I was more inclined towards using it to achieve SSO. So essentially what was required is a single user manager app that can act as a Provider<\/strong> (OAuth2 ?) and different applications (or Clients<\/strong>) that can authenticate themselves using this same user manager. An important part of SSO is, once you sign in to one of the client, you should automatically be authorized to access all the other clients (their login-protected sections\/modules). Similarly, logging out from one service should log out from all other services.<\/p>\n <\/p>\n To accomplish this, I found an excellent article by JoshSoftware<\/a> that solved my problem. Although I’d to change a lot of the code to make it Rails 4 compatible (from Rails 3). I’ve even uploaded the source code on Github:<\/p>\n Basically, OmniAuth on the client-side and some custom code on the provider-side is used to turn the provider into an OAuth2 provider, eventually accomplishing SSO. A lot of magic happens under the hood, so I decided to document the entire flow (even inside the gems) of the authentication process.<\/p>\n When a request is sent to the Implementation of this starts off by creating a custom OAuth2 Strategy which extends from If the user is not signed in then he's redirected to the Client's The request to To get the So the The At this stage the User has been redirected from the Client<\/strong> to Provider<\/strong> for authentication with the help of The The After authorization that creates a secret It is very important to note that before<\/strong> the Out of the object returned above, oauth2 gem creates an Finally the These info are fetched by executing the blocks passed to the respective methods (uid, info, extra, etc.). These methods will be defined in your custom strategy. The Now that we have the access token object with us, we can start making HTTP requests to the Provider<\/strong> from Client<\/strong> and pass the access token with it which can be checked against the DB in Provider for authorization. We can even do a Devise The oauth2 gem's In the existing code (hosted on Github, linked above), on the client-side, if the user is logged in successfully and the relevant session expires after sometime (or destroyed), the user will be redirected to the Provider where he'll be logged in. In this case the Provider will create a new access token and redirect the user to the Client which will again make an internal call to the Provider to obtain the access token. The code (hosted on Github above) can be modified ( That's all folks!<\/p>\n","protected":false},"excerpt":{"rendered":" Recently I had to implement Single Sign On (SSO) for one of the Rails app I’d been working on. Since Devise is already fairly popular to integrate an authentication system in Rails app, I was more inclined towards using it to achieve SSO. So essentially what was required is a single user manager app that … Continue reading “Single Sign On (SSO) for Multiple Applications with Devise, OmniAuth and Custom OAuth2 Implementation in Rails”<\/span><\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[12],"tags":[161,162,150,47,163],"_links":{"self":[{"href":"https:\/\/codetheory.in\/wp-json\/wp\/v2\/posts\/2168"}],"collection":[{"href":"https:\/\/codetheory.in\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/codetheory.in\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/codetheory.in\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/codetheory.in\/wp-json\/wp\/v2\/comments?post=2168"}],"version-history":[{"count":9,"href":"https:\/\/codetheory.in\/wp-json\/wp\/v2\/posts\/2168\/revisions"}],"predecessor-version":[{"id":2178,"href":"https:\/\/codetheory.in\/wp-json\/wp\/v2\/posts\/2168\/revisions\/2178"}],"wp:attachment":[{"href":"https:\/\/codetheory.in\/wp-json\/wp\/v2\/media?parent=2168"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/codetheory.in\/wp-json\/wp\/v2\/categories?post=2168"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/codetheory.in\/wp-json\/wp\/v2\/tags?post=2168"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}\n
Authentication by Client<\/h2>\n
Client<\/code> which is supposed to require authentication, it’ll check whether the user is logged in or not. If yes (
session<\/code> has data) then that’s fine otherwise the
Client<\/code> will redirect the user to a specific URL (of the Provider<\/strong>) where he can authenticate himself.<\/p>\n
OmniAuth::Strategies::OAuth2<\/code> (omniauth-oauth2<\/a>). The omniauth-oauth2 gem is basically a generic OAuth2 strategy for OmniAuth which basically means that it can serve as a building block for custom providers like omniauth-facebook (FB auth), omniauth-twitter (Twitter auth) and so on. So if we decide to call our provider Sso<\/strong> then we’ll have to create a custom strategy that’ll extend the same class, so something like this
class Sso < OmniAuth::Strategies::OAuth2<\/code>. The
Provider<\/code> must also be specified in
initializers\/omniauth.rb<\/code> so that it can be mounted as a Rack middleware.<\/p>\n
\/auth\/sso<\/code> URI. This is important because it triggers further authentication process. So this is a single entry point for all non-authenticated requests to a Client for authentications's sake as it'll trigger the entire chain of events further, required for authentication.<\/p>\n
\/auth\/sso<\/code> basically triggers the
request_phase<\/code> method of the custom strategy which calls the
request_phase<\/code> of
OmniAuth::Strategies::OAuth2<\/code> (parent class). How does this all happen ? So when the
OmniAuth::Builder<\/code> middleware was mounted (in
initializers\/omniauth.rb<\/code>), the call to
provider<\/code> mounted
OmniAuth::Strategies::Sso<\/code> which extends
OmniAuth::Strategies::OAuth2<\/code>. The initializer code looks something like this (for more context):<\/p>\n
\r\nAPP_ID = 'key'\r\nAPP_SECRET = 'secret'\r\n\r\nCUSTOM_PROVIDER_URL = 'http:\/\/localhost:3000'\r\n\r\nRails.application.config.middleware.use OmniAuth::Builder do\r\n provider :sso, APP_ID, APP_SECRET\r\nend\r\n<\/pre>\n
key<\/code> and
secret<\/code>, a Client<\/strong> has to be registered first in the Provider<\/strong>. So basically the DB has a
clients<\/code> table with these columns (to maintain all the registered clients):<\/p>\n
\r\nid | name | app_id | app_secret | created_at | updated_at\r\n<\/pre>\n
request_phase<\/code> of
OmniAuth::Strategies::OAuth2<\/code> (parent class) redirects the user to the
authorize_url<\/code> (on the Provider<\/strong>) specified in the custom strategy with certain parameters (looks something like
\/auth\/sso\/authorize?get_params<\/code>). Here\u2019s a sample URL:<\/p>\n
\r\nhttp:\/\/localhost:3000\/auth\/sso\/authorize?client_id=key&redirect_uri=http%3A%2F%2Flocalhost%3A3001%2Fauth%2Fsso%2Fcallback&response_type=code&state=f73bd089efa7722364d10ed36625502bd19783e27b628fdb\r\n<\/pre>\n
redirect_uri<\/code> (
http:\/\/localhost:3001\/auth\/sso\/callback<\/code> in this case) is generated by the
callback_path<\/code> method of
OmniAuth::Strategy<\/code> module (omniauth gem) which is included in the
OmniAuth::Strategies::OAuth2<\/code> class (omniauth-oauth2 gem). The callback path is something like
#{path_prefix}\/#{name}\/callback<\/code> (evaluates to
auth\/sso\/callback<\/code>) by default (should be configurable).<\/p>\n
Authentication by Provider<\/h2>\n
authorize_url<\/code>. As already mentioned this URL looks something like this
\/auth\/sso\/authorize?get_params<\/code> and is protected by Devise. So if the user is not logged into the Provider<\/strong> he'll be taken to the signin\/signup flow\/page (
http:\/\/provider\/user\/sign_in<\/code> for instance) which once completed redirects the user back to this
authorize_url<\/code>. This post-signin\/signup redirection happens via Devise automagically since it maintains the previous URL requested (that led to the sign in page) in
session['user_return_to']<\/code>. Once the user is authenticated (or if he was already authenticated by the Provider<\/strong>) then the action mapped to the
\/auth\/sso\/authorize<\/code> route is executed.<\/p>\n
authorize<\/code> action gets a param called
state<\/code> (if you notice the URL above) which is generated inside
OmniAuth::Strategies::OAuth2<\/code> (in omniauth-oauth2 of Client<\/strong>) like this
options.authorize_params[:state] = SecureRandom.hex(24)<\/code>. The
state<\/code> value is be later used in the
callback_phase<\/code> of omniauth-oauth2 for a CSRF check (protection). Basically the action creates an entry in a table called
access_grants<\/code> with these fields:<\/p>\n
\r\ncode | access_token | refresh_token | access_token_expires_at | user_id | client_id | state | created_at\r\n<\/pre>\n
code<\/code>,
access_token<\/code>,
refresh_token<\/code> fields are generated using
SecureRandom.hex(16)<\/code>. After creating an access grant, the user is redirected to the callback URL with the same
state<\/code> param that was sent while authentication redirection and the newly generated
code<\/code> param. So then new redirection URL will be something like this -
redirect_uri + \"?code=#{code}&response_type=code&state=#{state}\"<\/code> where the
redirect_uri<\/code> is the same callback URL as above retrieved via
params['redirect_uri']<\/code>.<\/p>\n
Client Callback Processing<\/h2>\n
code<\/code>, the user is redirected to the callback URL which is on the Client<\/strong>. The callback URL looks something like this
\/auth\/sso\/callback?code=xxx&response_type=code&state=xxx<\/code>. It is intercepted by
OmniAuth::Strategies::Sso < OmniAuth::Strategies::OAuth2<\/code> (custom OAuth2 strategy) which was setup as a middleware by
OmniAuth::Builder<\/code>'s
provider<\/code> method call. To recap,
OmniAuth::Strategies::OAuth2<\/code> includes
OmniAuth::Strategies<\/code> and
OmniAuth::Builder<\/code> loads
OmniAuth::Strategies::Sso<\/code> as a middleware. On interception, the
call<\/code> method on
OmniAuth::Strategies<\/code> is called (because it is included by
OmniAuth::Strategies::OAuth2<\/code> and it's a rack middleware) which triggers the
callback_call<\/code> method which in turn calls the
callback_phase<\/code> method whose job is to basically set
omniauth.auth<\/code> env variable (
env['omniauth.auth']<\/code>) to an
AuthHash<\/code>. While setting this AuthHash a couple of methods are called that should be defined in the custom strategy implementation like
uid<\/code>,
info<\/code>,
credentials<\/code> and
extra<\/code>.<\/p>\n
callback_phase<\/code> of
OmniAuth::Strategies<\/code> module, the same of
OmniAuth::Strategies::OAuth2<\/code> is called (since it includes
OmniAuth::Strategies<\/code> and its
callback_phase<\/code> calls
super<\/code>). This method does a couple of interesting things. It triggers
build_access_token<\/code> which basically makes a request to the Provider with the
code<\/code> that had been received. What this method does is, it uses the
oauth2<\/code> library to get the access token from the
token_url<\/code> specified in the custom strategy (otherwise default is
\/oauth\/token<\/code>). So it hits the
token_url<\/code> with the
code<\/code>,
client_id<\/code>,
client_secret<\/code> to receive the access token. Feel free to check the rails developments logs while you try to login for all other params\/info. Once the Provider gets this request, it checks if a client record exists in its DB with the
client_id<\/code> and
client_secret<\/code> and whether the
code<\/code> actually is associated with any
AccessGrant<\/code> which is associated to a User. In the end it will return a JSON object:<\/p>\n
\r\n{"access_token": "access_token", "refresh_token" => "refresh_token", "expires_in": Devise.timeout_in.to_i}\r\n<\/pre>\n
AccessToken<\/code> object (accessed as
access_token<\/code> across codebase) which sets instance variable for each key and renames
access_token<\/code> to
token<\/code> - so it's accessed as
access_token_ob.token<\/code>.<\/p>\n
token_url<\/code> will hit the action mapped to it in
routes.rb<\/code> which has to be a custom implementation. There
env['omniauth.auth']<\/code> will contain an object of the AuthHash<\/a> type (because of that call to
callback_phase<\/code> of
OmniAuth::Strategies<\/code> module). Here's a sample dump of it to show how it'll look like:<\/p>\n
\r\n{"provider"=>"sso",\r\n "uid"=>"2",\r\n "info"=>{"email"=>"rishabh.pugalia@gmail.com"},\r\n "credentials"=>\r\n {"token"=>"7c21854636f5d71c3dde2bde2cc93e16",\r\n "refresh_token"=>"5f5770f7028eefae6e775cba8aef7a5a",\r\n "expires_at"=>1441737106,\r\n "expires"=>true},\r\n "extra"=>{"first_name"=>"", "last_name"=>""}}\r\n<\/pre>\n
credentials<\/code> is specified in
OmniAuth::Strategies::OAuth2<\/code> (omniauth-oauth2).<\/p>\n
Using the Access Token<\/h2>\n
sign_in<\/code><\/a> call on the user object if one is found via the
access_token<\/code>.<\/p>\n
AccessToken<\/code> object has a concept of refreshing tokens too. Right after the
build_access_token<\/code> is called and the access token is fetched, a call is made to
access_token.refresh!<\/code> if the access token has expired. So if the response of access token has
expires_at<\/code> which is less than current time (
expires_at < Time.now.to_i<\/code>) then the gem will make a new request to get the access token (same flow as above) but this time the token sent for verification will be a
refresh_token<\/code> which was generated the first time when access was granted and it should be used to return the old access token (might want to regenerate that). The other difference will be in the
grant_type<\/code> param which will be
refresh_token<\/code> this time compared to
authorization_code<\/code> earlier. This call should also re-set (update) the expiry time of the access token.<\/p>\n
AuthController#authorize<\/code><\/a>) in a way where if the user is already logged in to the Provider and has a **recently used** access token, in the case the expiry of that token can be re-set to a future date and re-used.<\/p>\n
Overall Conceptual Flow<\/h2>\n
\n