Workbench authentication process

  1. In workbench, when the browser goes to a page, it checks for a session or ?api_token=xxx in the URL for the API token. If no API token is found, the brower is directed to the workbench "welcome" page workbench/app/views/users/welcome.html.erb
  2. In workbench, the "welcome" page has a "log in" button that directs the browser to the API server login URL, with a ?return_to=xxx link embedded in the URL.
    1. Workbench may provide a auth_provider parameter in the "log in" button form in order to select an SSO provider, such as "Google OAuth2" or "Google OpenId"
  3. In API server, the 'login' endpoint goes to UserSessionsController#login in the API server. This redirects the browser to /auth/joshid?return_to=xxx?auth_provider=zzz
  4. In API server, /auth/joshid is intercepted by the OmniAuth Rack middleware and invokes the josh_id OmniAuth strategy.
    1. The josh_id OmniAuth strategy is implemented in arvados/services/api/lib/josh_id.rb and is a subclass of OmniAuth::Strategies::OAuth2
    2. OmniAuth starts the "request_phase" of OmniAuth::Strategies::OAuth2. This redirects the browser to #{options[:custom_provider_url]}/auth/josh_id/authorize?return_to=xxx?auth_provider=zzz using CUSTOM_PROVIDER_URL defined in arvados/services/api/config/initializers/omniauth.rb
  5. In sso-provider, /auth/josh_id/authorize is routed to AuthController#authorize, and is intercepted by before_filter :authenticate_user! (part of the devise gem)
    1. save_auth_provider records the :auth_provider parameters in the session
    2. devise :omniauthable, :omniauth_providers => [:google_oauth2] configures sso-provider/app/models/user.rb to use the "google_oauth2" strategy
    3. authenticate_user! is not explicitly defined but instead monkey patched into the controller in devise/lib/devise/controllers/helper.rb
    4. authenticate_user! calls warden.authenticate! (warden/lib/warden/proxy.rb) with a scope of :user (warden is another Rack-based authentication gem)
    5. Warden proxy tries the TokenAuthenticatable and DatabaseAuthenticatable strategies, but these strategies fail and it raises a :warden exception. This causes it to call failure_app which is set up in devise/lib/devise.rb to be Devise::Delegator.new
    6. This calls devise/lib/devise/failure_app.rb which saves the attempted path in the session ("user_return_to") using store_location!, then redirects the browser to new_user_session_path (/users/sign_in)
  6. In sso-provider /users/sign_in routes to the SSO SessionsController#new which subclasses Devise::SessionsController
    1. SessionsController#new redirects the browser to /users/auth/#{session[:auth_provider]}
  7. In sso-provider, OmniAuth intercepts /users/auth/:auth_provider
    1. OmniAuth is configured with a path prefix of /users/auth by devise
    2. sso-provider/config/initializers/devise.rb configures the auth_providers
    3. :google_oauth2
      1. Redirects to client_options.site + client_options.authorize_url which is https://accounts.google.com/o/oauth2/auth
      2. The user gets a Google login page and enters their information, this directs the browser to the sso-server at /users/auth/:auth_provider/callback after a successful login (after more redirects within Google)
      3. In sso-provider, Omniauth intercepts this and calls callback_phase in the provider's omniauth module
      4. This uses the callback code to initiate a request to Google for an access token and other user information from https://accounts.google.com/o/oauth2/token
    4. :ldap
      1. Redirects to /users/ldap_sign_in (page template app/views/users/ldap_sign_in.html.erb)
      2. User enters their information, it is submitted to to /users/auth/ldap/callback
      3. Omniauth callback contacts the LDAP server and performs directory lookup with username/password
    5. Built-in user database
      1. Presents built-in login page (page template app/views/sessions/new.html.erb)
      2. Creates session from local db login
  8. Successful omniauth callback provisions the Rack environment with the user information returned by oauth2/ldap callback.
  9. Request handling continues to sso-provider where it routes to Users::OmniauthCallbacksController#(auth_method)
    1. google_oauth2 and ldap callbacks call User.authenticate model which finds a user record with a matching omniauth uid (the identity_url column in the users table), or creates and saves a new user record with the uid.
    2. The callback also saves the first_name and last_name of the user.
    3. This calls sign_in_and_redirect defined in devise/lib/devise/controllers/helpers.rb
    4. This calls set_user in Warden, which uses Warden::SessionSerializer to save the user associated with the session
    5. This redirects the browser to stored_location_for which returns the value of user_return_to in the session, which is /auth/josh_id/authorize from step 5.
  10. In sso-provider, /auth/josh_id/authorize routes to AuthController#authorize, and passes authenticate_user! because the there is now a user associated with the session.
    1. authorize builds an AccessGrant using the state token from the request
    2. authorize redirects the browser to params[:redirect_uri] which is /auth/joshid/callback (this is the oauth callback on the API server, saved from step 5)
  11. In API server, /auth/joshid/callback is intercepted by OmniAuth::Strategies::OAuth2 and calls callback_phase.
    1. OmniAuth::Strategies::OAuth2 creates a ::OAuth2::Client and calls get_token to convert the "authorization_code" into a "oauth_token"
    2. The API server makes a request to the sso-provider at /oauth/token
  12. In the sso-provider, /oauth/token is routed to AuthController#access_token
    1. This first checks Client.authenticate which verifies that client_id (app_id) and client_secret (app_secret) are recognized
    2. Next it calls AccessGrant.authenticate which verifies that code and client_id are recognized
    3. This renders an JSON API response with the access_token, refresh_token and expires_in fields which are returned to the API server.
  13. The API server, back in OmniAuth::Strategies::OAuth2, receives the response and saves the access_token.
    1. Request processing continues and routes to UserSessionsController#create
    2. API server gets the OmniAuth object and looks up the Arvados API user by identity_url
    3. The session is provisioned with the Arvados user id
    4. if params.has_key?(:return_to) then it calls send_api_token_to
    5. send_api_token_to creates a new ApiClientAuthorization
    6. It redirects the browser to the :return_to after adding api_token=xxx to the query portion of return_to
  14. The browser is finally redirected to workbench to with api_token=xxx
    1. Workbench adds the api_token to the session, and redirects the browser one last time to the same location with ?api_token stripped from the URL