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
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.
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"
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
In API server, /auth/joshid is intercepted by the OmniAuth Rack middleware and invokes the josh_id OmniAuth strategy.
The josh_id OmniAuth strategy is implemented in arvados/services/api/lib/josh_id.rb and is a subclass of OmniAuth::Strategies::OAuth2
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
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)
save_auth_provider records the :auth_provider parameters in the session
devise :omniauthable, :omniauth_providers => [:google_oauth2] configures sso-provider/app/models/user.rb to use the "google_oauth2" strategy
authenticate_user! is not explicitly defined but instead monkey patched into the controller in devise/lib/devise/controllers/helper.rb
authenticate_user! calls warden.authenticate! (warden/lib/warden/proxy.rb) with a scope of :user (warden is another Rack-based authentication gem)
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
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)
In sso-provider /users/sign_in routes to the SSO SessionsController#new which subclasses Devise::SessionsController
SessionsController#new redirects the browser to /users/auth/#{session[:auth_provider]}
In sso-provider, OmniAuth intercepts /users/auth/:auth_provider
OmniAuth is configured with a path prefix of /users/auth by devise
sso-provider/config/initializers/devise.rb configures the auth_providers
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)
In sso-provider, Omniauth intercepts this and calls callback_phase in the provider's omniauth module
Successful omniauth callback provisions the Rack environment with the user information returned by oauth2/ldap callback.
Request handling continues to sso-provider where it routes to Users::OmniauthCallbacksController#(auth_method)
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.
The callback also saves the first_name and last_name of the user.
This calls sign_in_and_redirect defined in devise/lib/devise/controllers/helpers.rb
This calls set_user in Warden, which uses Warden::SessionSerializer to save the user associated with the session
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.
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.
authorize builds an AccessGrant using the state token from the request
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)
In API server, /auth/joshid/callback is intercepted by OmniAuth::Strategies::OAuth2 and calls callback_phase.
OmniAuth::Strategies::OAuth2 creates a ::OAuth2::Client and calls get_token to convert the "authorization_code" into a "oauth_token"
The API server makes a request to the sso-provider at /oauth/token
In the sso-provider, /oauth/token is routed to AuthController#access_token
This first checks Client.authenticate which verifies that client_id (app_id) and client_secret (app_secret) are recognized
Next it calls AccessGrant.authenticate which verifies that code and client_id are recognized
This renders an JSON API response with the access_token, refresh_token and expires_in fields which are returned to the API server.
The API server, back in OmniAuth::Strategies::OAuth2, receives the response and saves the access_token.
Request processing continues and routes to UserSessionsController#create
API server gets the OmniAuth object and looks up the Arvados API user by identity_url
The session is provisioned with the Arvados user id
if params.has_key?(:return_to) then it calls send_api_token_to
send_api_token_to creates a new ApiClientAuthorization
It redirects the browser to the :return_to after adding api_token=xxx to the query portion of return_to
The browser is finally redirected to workbench to with api_token=xxx
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