Авторизация в 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
    

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

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

    Советы, вопросы и замечания принимаются в комментариях.
    Southbridge
    266,00
    Обеспечиваем стабильную работу серверов
    Поделиться публикацией

    Похожие публикации

    Комментарии 5

      0
      > Но это совсем не решения, т.к. открывают дыру в безопасности сайта.

      А какая дыра открывается, если отключить проверку токена для sessions#create? Я не могу с ходу придумать, почему этого делать нельзя. Для большей уверенности можно просто добавить проверку реферера.
        +1
        Насколько я понимаю, токен который генериуется по определенному правилу абсолютно бесполезен
          0
          Верно. Достаточно было отключить проверку для этого действия + проверять реферер а лучше Origin
        +1
        printercu, Tonkonozhenko, Chikey благодарю за комментарии.

        Отвечу одним сообщением на ваши отзывы.

        А какая дыра открывается, если отключить проверку токена для sessions#create? Я не могу с ходу придумать, почему этого делать нельзя.

        Про XSS есть хорошая статья на хабре — habrahabr.ru/post/66057
        Я в атаках не спец, но решил не отказываться от базовой проверки авторизованности запросов.

        Чем я руководствовался при написании плагина?

        • Нужно сделать возможность логиниться в Redmine со внешних сайтов.
          Их может быть несколько, т.к. лэндингов у компании может быть несколько.
        • Если для авторизации POST-запросов придумали токен, значит стоит его все же сохранить.
        • Это плагин для Redmine. Поэтому любой желающий может добавить себе и выполнить соответствующую настройку на стороне сайта.
          Плагин подразумевает универсальность.

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

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

        Это добавило универсальности коду, т.к. не нужно домены прописывать и исключило возможность копирования токена с целью использования его для отправки запросов с иных ресурсов.
          0
          Такое решение бессмысленно. Как «работает» xss токен:
          — токен лежит в сессии (зашифрованный в куках или редисе, например);
          — вы отдаете его только пользователю с этой сессией (это ключевой момент);
          — при запросах от этого пользователя вы проверяете, совпадает ли токен из запроса с токеном из сессии.

          В вашей реализации токен для всех один. Злоумышленнику нужно его 1 раз узнать, и он может его использовать, пока вы конфиг не поменяете, размещая на своих сайтах формы. Реферер-то вы просто проверяете на совпадение с текстом в токене (читай: сейчас у вас токен ничего не защищает).

          Для чего нужен токен: чтобы от имени пользователя не слали запросы с других сайтов. Например, на сайте злоумышленника кнопка «скачать без смс и регистрации» отправляет аяксом POST http://your-domain/admin/destroy_all (ну или просто создания спам-поста от имени пользователя).

          Если же вы уберете проверку токена для входа, то всё, что сможет сделать злоумышленник — залогинить пользователя. По-моему, это не страшно.

          Если нужно запретить логин со сторонних сайтов, то нужно
          — либо часто обновлять токен и подписывать его. Для этого на всех сайтах надо логику подписи делать, это не сложно, но без нее никак.
          — либо просто использовать белый список рефереров.

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

        Самое читаемое