Авторизация в Redmine с другого сайта

На сайте centos-admin.ru дизайнер придумал очень здоровский эффект для формы логина. Идея формы состоит в том, что пользователь вводит свои логин и пароль в Redmine и попадает авторизованным на свою страничку.

Все бы здорово, но в Ruby on Rails (на коих Redmine сделан) прямые POST запросы с внешних сайтов не принимаются — для успешного запроса нужен авторизационный токен.

Сей токен генерируется rails-приложениями в автоматическом режиме, хранится в cookies. В связи с этим сперва думал в iframe загружать сайт с Redmine-ом и из cookies брать нужный ключ. Но как-то это совсем не rails-way.

Самое простое решение — слегка пропатчить Redmine — добавить возможность обработки запросов с внешних ресурсов. Благо в Redmine все для этого есть — можно написать небольшой плагин, который и будет решать эту задачу.

Что будет делать плагин?


Вопрос казалось бы простой, но нужно помнить о сохранении безопасности пользовательских данных.

Первые решения со Stackoverflow предлагали отключать проверку токена для конкретного экшена. Но это совсем не решения, т.к. открывают дыру в безопасности сайта.

Соответственно остается вариант использовать самостоятельно генерируемый токен, на стороне Redmine его проверять и в случае успешной проверки, проводить авторизацию.

Как генерировать токен?


Самый простой вариант — использовать любую строку символов, но мне показалось, что этого мало для безопасной авторизации. Так как простой токен можно перехватить и использовать его для отправки данных с неавторизованных ресурсов.

Поэтому я решил в авторизационный токен добавить домен, с которого отправляется запрос и информацию о текущей дате.

На стороне сайта в хэлперах создаем метод

  def authenticity_token
    token = Settings.redmine_remote_login_token
    Base64.encode64 "#{token}-#{request.host}-#{Date.today}"
  end


и затем его используем в форме

<%= form_tag 'http://factory.southbridge.ru/remote_login', authenticity_token: authenticity_token do %>


Что происходит на стороне Redmine?


На стороне Redmine нужно добавить маршрут для обработки POST запросов на путь /remote_login

post 'remote_login', to: 'account#remote_login'


И слегка пропатчить AccountController, добавив к нему экшен remote_login:

module RemoteLogin
  module AccountControllerPatch
    def self.included(base) # :nodoc:
      base.class_eval do
        unloadable
        skip_before_filter :verify_authenticity_token, only: :remote_login
        before_filter :verify_remote_authenticity_token, only: :remote_login

        def remote_login
          authenticate_user
        rescue AuthSourceException => e
          logger.error "An error occured when authenticating #{params[:username]}: #{e.message}"
          render_error :message => e.message
        end

        private

        def verify_remote_authenticity_token
          uri = URI.parse(request.env['HTTP_REFERER'])
          token = Setting.plugin_redmine_remote_login['token']

          unless Base64.decode64(params['authenticity_token']) == "#{token}-#{uri.host}-#{Date.today}"
            if logger && log_warning_on_csrf_failure
              logger.warn "Can't verify CSRF token authenticity"
            end
            handle_unverified_request
          end
        end
      end
    end

  end
end
AccountController.send(:include, RemoteLogin::AccountControllerPatch)


Здесь используется отмена стандартной проверки токена

skip_before_filter :verify_authenticity_token, only: :remote_login


и вместо нее выполняется написанная нами

before_filter :verify_remote_authenticity_token, only: :remote_login


Вот собственно и весь плагин. Небольшой, но решает важную задачу — посетителям сайта удобно заходить в свой личный кабинет, не допуская при этом авторизацию с левых ресурсов.

С кодом плагина можно ознакомиться тут.

Советы, вопросы и замечания принимаются в комментариях.

© Habrahabr.ru