Merge branch '14718-api-login-salted-token'
authorLucas Di Pentima <ldipentima@veritasgenetics.com>
Tue, 29 Jan 2019 09:30:10 +0000 (06:30 -0300)
committerLucas Di Pentima <ldipentima@veritasgenetics.com>
Tue, 29 Jan 2019 09:30:10 +0000 (06:30 -0300)
Closes #14718

Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <ldipentima@veritasgenetics.com>

lib/controller/handler_test.go
services/api/app/controllers/user_sessions_controller.rb
services/api/app/models/api_client_authorization.rb
services/api/test/functional/user_sessions_controller_test.rb
services/api/test/integration/user_sessions_test.rb

index 746b9242f2198ee3c3000c808771047d4aa1c77c..f11228a31350b93f2da70a7b5ab46b8926a47b06 100644 (file)
@@ -128,7 +128,7 @@ func (s *HandlerSuite) TestProxyRedirect(c *check.C) {
        resp := httptest.NewRecorder()
        s.handler.ServeHTTP(resp, req)
        c.Check(resp.Code, check.Equals, http.StatusFound)
-       c.Check(resp.Header().Get("Location"), check.Matches, `https://0.0.0.0:1/auth/joshid\?return_to=foo&?`)
+       c.Check(resp.Header().Get("Location"), check.Matches, `https://0.0.0.0:1/auth/joshid\?return_to=%2Cfoo&?`)
 }
 
 func (s *HandlerSuite) TestValidateV1APIToken(c *check.C) {
index 020dfa53b83a6ba645a79a1696b84968144cc0cd..1889d74eaa891dc980889cfd00491f54935ee08f 100644 (file)
@@ -95,7 +95,15 @@ class UserSessionsController < ApplicationController
 
     @redirect_to = root_path
     if params.has_key?(:return_to)
-      return send_api_token_to(params[:return_to], user)
+      # return_to param's format is 'remote,return_to_url'. This comes from login()
+      # encoding the remote=zbbbb parameter passed by a client asking for a salted
+      # token.
+      remote, return_to_url = params[:return_to].split(',', 2)
+      if remote !~ /^[0-9a-z]{5}$/ && remote != ""
+        return send_error 'Invalid remote cluster id', status: 400
+      end
+      remote = nil if remote == ''
+      return send_api_token_to(return_to_url, user, remote)
     end
     redirect_to @redirect_to
   end
@@ -119,8 +127,9 @@ class UserSessionsController < ApplicationController
   # to save the return_to parameter (if it exists; see the application
   # controller). /auth/joshid bypasses the application controller.
   def login
-    auth_provider = if params[:auth_provider] then "auth_provider=#{CGI.escape(params[:auth_provider])}" else "" end
-
+    if params[:remote] !~ /^[0-9a-z]{5}$/ && !params[:remote].nil?
+      return send_error 'Invalid remote cluster id', status: 400
+    end
     if current_user and params[:return_to]
       # Already logged in; just need to send a token to the requesting
       # API client.
@@ -128,15 +137,20 @@ class UserSessionsController < ApplicationController
       # FIXME: if current_user has never authorized this app before,
       # ask for confirmation here!
 
-      send_api_token_to(params[:return_to], current_user)
-    elsif params[:return_to]
-      redirect_to "/auth/joshid?return_to=#{CGI.escape(params[:return_to])}&#{auth_provider}"
-    else
-      redirect_to "/auth/joshid?#{auth_provider}"
+      return send_api_token_to(params[:return_to], current_user, params[:remote])
     end
+    p = []
+    p << "auth_provider=#{CGI.escape(params[:auth_provider])}" if params[:auth_provider]
+    if params[:return_to]
+      # Encode remote param inside callback's return_to, so that we'll get it on
+      # create() after login.
+      remote_param = params[:remote].nil? ? '' : params[:remote]
+      p << "return_to=#{CGI.escape(remote_param + ',' + params[:return_to])}"
+    end
+    redirect_to "/auth/joshid?#{p.join('&')}"
   end
 
-  def send_api_token_to(callback_url, user)
+  def send_api_token_to(callback_url, user, remote=nil)
     # Give the API client a token for making API calls on behalf of
     # the authenticated user
 
@@ -147,19 +161,24 @@ class UserSessionsController < ApplicationController
         find_or_create_by(url_prefix: api_client_url_prefix)
     end
 
-    api_client_auth = ApiClientAuthorization.
+    @api_client_auth = ApiClientAuthorization.
       new(user: user,
           api_client: @api_client,
           created_by_ip_address: remote_ip,
           scopes: ["all"])
-    api_client_auth.save!
+    @api_client_auth.save!
 
     if callback_url.index('?')
       callback_url += '&'
     else
       callback_url += '?'
     end
-    callback_url += 'api_token=' + api_client_auth.token
+    if remote.nil?
+      token = @api_client_auth.token
+    else
+      token = @api_client_auth.salted_token(remote: remote)
+    end
+    callback_url += 'api_token=' + token
     redirect_to callback_url
   end
 
index 53ae6af46426cadd55bf7ec4ae1cc94659ef1c0f..39253e1036ba9a52b2070f9e0a7d4043fecb2d43 100644 (file)
@@ -236,6 +236,13 @@ class ApiClientAuthorization < ArvadosModel
     'v2/' + uuid + '/' + api_token
   end
 
+  def salted_token(remote:)
+    if remote.nil?
+      token
+    end
+    'v2/' + uuid + '/' + OpenSSL::HMAC.hexdigest('sha1', api_token, remote)
+  end
+
   protected
 
   def permission_to_create
index f7021cf9def1487d834186339fc38eb8cea576e7..e3048159f4e7a99f025fca9153f2f09cf4d07863 100644 (file)
@@ -17,4 +17,22 @@ class UserSessionsControllerTest < ActionController::TestCase
     assert_not_nil assigns(:api_client)
   end
 
+  test "login with remote param returns a salted token" do
+    authorize_with :inactive
+    api_client_page = 'http://client.example.com/home'
+    remote_prefix = 'zbbbb'
+    get :login, return_to: api_client_page, remote: remote_prefix
+    assert_response :redirect
+    api_client_auth = assigns(:api_client_auth)
+    assert_not_nil api_client_auth
+    assert_includes(@response.redirect_url, 'api_token='+api_client_auth.salted_token(remote: remote_prefix))
+  end
+
+  test "login with malformed remote param returns an error" do
+    authorize_with :inactive
+    api_client_page = 'http://client.example.com/home'
+    remote_prefix = 'invalid_cluster_id'
+    get :login, return_to: api_client_page, remote: remote_prefix
+    assert_response 400
+  end
 end
index 0497c6a7d56294ae3d0841db5acd8ef9a441d809..f5085999ec5681d9da56f5244bd204e5473246dd 100644 (file)
@@ -5,11 +5,15 @@
 require 'test_helper'
 
 class UserSessionsApiTest < ActionDispatch::IntegrationTest
-  def client_url
-    'https://wb.example.com'
+  # remote prefix & return url packed into the return_to param passed around
+  # between API and SSO provider.
+  def client_url(remote: nil)
+    url = ',https://wb.example.com'
+    url = "#{remote}#{url}" unless remote.nil?
+    url
   end
 
-  def mock_auth_with(email: nil, username: nil, identity_url: nil)
+  def mock_auth_with(email: nil, username: nil, identity_url: nil, remote: nil, expected_response: :redirect)
     mock = {
       'provider' => 'josh_id',
       'uid' => 'https://edward.example.com',
@@ -24,9 +28,14 @@ class UserSessionsApiTest < ActionDispatch::IntegrationTest
     mock['info']['username'] = username unless username.nil?
     mock['info']['identity_url'] = identity_url unless identity_url.nil?
     post('/auth/josh_id/callback',
-         {return_to: client_url},
+         {return_to: client_url(remote: remote)},
          {'omniauth.auth' => mock})
-    assert_response :redirect, 'Did not redirect to client with token'
+
+    errors = {
+      :redirect => 'Did not redirect to client with token',
+      400 => 'Did not return Bad Request error',
+    }
+    assert_response expected_response, errors[expected_response]
   end
 
   test 'assign username from sso' do
@@ -61,14 +70,25 @@ class UserSessionsApiTest < ActionDispatch::IntegrationTest
 
   test 'create new user during omniauth callback' do
     mock_auth_with(email: 'edward@example.com')
-    assert_equal(0, @response.redirect_url.index(client_url),
+    assert_equal(0, @response.redirect_url.index(client_url.split(',', 2)[1]),
                  'Redirected to wrong address after succesful login: was ' +
-                 @response.redirect_url + ', expected ' + client_url + '[...]')
+                 @response.redirect_url + ', expected ' + client_url.split(',', 2)[1] + '[...]')
     assert_not_nil(@response.redirect_url.index('api_token='),
                    'Expected api_token in query string of redirect url ' +
                    @response.redirect_url)
   end
 
+  test 'issue salted token from omniauth callback with remote param' do
+    mock_auth_with(email: 'edward@example.com', remote: 'zbbbb')
+    api_client_auth = assigns(:api_client_auth)
+    assert_not_nil api_client_auth
+    assert_includes(@response.redirect_url, 'api_token=' + api_client_auth.salted_token(remote: 'zbbbb'))
+  end
+
+  test 'error out from omniauth callback with invalid remote param' do
+    mock_auth_with(email: 'edward@example.com', remote: 'invalid_cluster_id', expected_response: 400)
+  end
+
   # Test various combinations of auto_setup configuration and email
   # address provided during a new user's first session setup.
   [{result: :nope, email: nil, cfg: {auto: true, repo: true, vm: true}},