2017–07–19 13:59:54 -0700

Для моего последнего проекта в Программе веб-разработчиков полного стека Flatiron School я решил создать одностраничное приложение с использованием React и Redux. Я написал базовый серверный API с использованием Rails для обеспечения хранения и извлечения данных, а затем приступил к работе с внешним интерфейсом. Создание API было довольно простым делом, и здесь мы не будем его подробно обсуждать.

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

Я поискал и нашел пару многообещающих: redux-auth и react-native-oauth, но они казались излишне сложными. Полагаю, частично это было связано с тем, что я не понимал JWT так хорошо, как должен.

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

Итак, прямо сейчас вы, вероятно, думаете, что это действительно непросто. Но большая часть сложности заключена в прямоугольниках с красным текстом, с которыми без особых усилий обработали гем Omniauth или devise. По общему признанию, разработка - это не совсем прогулка по парку, но она достаточно популярна, вы, возможно, уже сталкивались с ней. Лично для этого проекта devise является излишним, так как мне нужен был только OAuth, и мне не нужно было сбрасывать пароль, двухфакторную аутентификацию и т. Д., Поэтому я выбрал гем omniauth.

1. Запуск процесса

Это так же просто, как предоставить пользователю кнопку для нажатия, указывающую, что он хочет войти в систему. Я выбрал поддержку входа через Facebook или Github, но вы можете использовать любую службу OAuth, которую вы выберете, при условии, что она поддерживается omniauth. Каждая кнопка вызывает действие #authenticate в SessionsController, которое перенаправляет на соответствующий путь аутентификации omniauth.

def authenticate
  redirect_to '/auth/facebook' if params[:type] == 'facebook'
  redirect_to '/auth/github' if params[:type] == 'github'
end

2. Аутентифицируйте пользователя с помощью OmniAuth.

API аутентифицирует пользователя с помощью гема omniauth. Я не буду вдаваться в подробности, так как самоцвет хорошо задокументирован здесь.

3. Найдите или создайте пользователя

API использует результат обратного вызова omniauth для поиска или создания пользователя. В SessionsController у нас есть:

def create
    user = User.find_or_create_by(uid: auth['uid']) do |u|
      u.name = auth['info']['name']
      u.email = auth['info']['email']
      u.image = auth['info']['image']
    end
  ... 
end
def auth
  request.env['omniauth.auth']
end

Это все, что нам нужно для аутентификации пользователя и определения его идентификатора.

ПРИМЕЧАНИЕ. Если вы смешиваете вход в систему OAuth с «традиционным» именем пользователя и паролями, вам следует создать случайный пароль для учетных записей, созданных с помощью OAuth. Иначе у них его не будет!

Затем мы должны отправить данные обратно клиенту в форме JWT.

4. Создайте JWT и отправьте его клиенту.

JWT - это веб-токен JSON, который является отраслевым стандартом для безопасной передачи данных с цифровой подписью. Чтобы создать этот JWT, API объединяет данные, заголовок и секретный ключ и смешивает все это в защищенный пакет, который может быть передан клиенту.

Есть много ресурсов о том, как создавать и использовать JWT:

Мое любимое на самом деле было написано студентом / инструктором Flatiron:

На стороне сервера я использовал jwt gem для кодирования и декодирования токенов. Я написал вспомогательный класс для выполнения грязной работы:

class Auth
  def self.encode_uid(user_id)
    payload = { user_id: user_id }
    JWT.encode payload, ENV['AUTH_SECRET'], 'HS256'
  end
  
  def self.decode_uid(token)
    payload = JWT.decode token, ENV['AUTH_SECRET'], true,
              { :algorithm => 'HS256' }
    payload[0]['user_id']
  end
end

Обратите внимание, что мы храним секрет в переменной окружения, чтобы случайно не загрузить его на github!

Теперь, когда мы создали класс Auth, вернуть идентификатор пользователя клиенту внутри JWT очень просто. В нашем SessionsController действие create теперь становится следующим:

def create
  user = User.find_or_create_by(uid: auth['uid']) do |u|
    u.name = auth['info']['name']
    u.email = auth['info']['email']
    u.image = auth['info']['image']
  end
  if user
    jwt = Auth.encode_uid(user.uid)
    redirect_to(ENV['DIVE_LOG_CLIENT_URL'] + "?token=#{jwt}")
  end
end

ПРИМЕЧАНИЕ. JWT будет отправляться клиенту в строке запроса, которая представляет собой обычный текст. Любой, кто получит ваш JWT, может использовать его для доступа к API и вашим данным. Чтобы избежать этого, вы должны использовать на клиенте конечную точку https, которая будет шифровать строку запроса.

5. Использование JWT клиентом.

На стороне клиента JWT хранится в хранилище сеанса:

sessionStorage.setItem(‘jwt’, jwt)

Затем все, что нужно сделать клиенту, - это поместить его в заголовок для каждого запроса, который он делает к api. Например, метод get выглядит так:

get(url) {  
  const jwt = sessionStorage.getItem('jwt');
  const headers =  {
    'Accept': 'application/json',
    'Content-Type': 'application/json',
    'Authorization': `Bearer: ${jwt}`
  }
  return fetch(`${API_URL}${url}`, {
    method: 'GET',
    headers: headers
  }).then(response => (response.json()));
}

6. Использование JWT для идентификации текущего пользователя.

В контроллере приложения у нас есть несколько вспомогательных методов, которые выполняют аутентификацию и извлекают current_user:

class ApplicationController < ActionController::API  
  before_action :authenticate
  def logged_in?
    !!current_user
  end
  def current_user
    return @current_user if @current_user
    if auth_present?
      uid = Auth.decode_uid(read_token_from_request)
      @current_user = User.find_by({uid: uid})
      return @current_user if @current_user
    end
  end
  def authenticate
    render json: {error: "unauthorized"}, 
                 status: 401 unless logged_in?
  end
private
  def read_token_from_request
    token = request.env["HTTP_AUTHORIZATION"]
                   .scan(/Bearer: (.*)$/).flatten.last
  end
  def auth_present?
    !!request.env.fetch("HTTP_AUTHORIZATION", "")
             .scan(/Bearer/).flatten.first
  end
end

Резюме

Это довольно простая реализация OAuth для приложения React с бэкэндом rails. Однако он не лишен недостатков:

  • Перенаправление от API обратно к клиенту вызывает полную перезагрузку страницы. Это, вероятно, не о чем беспокоиться, но имейте это в виду.
  • Хотя HTTPS становится все более распространенным явлением, он может быть недоступен для вас. Я ищу возможные улучшения. Одна из идей состоит в том, чтобы случайным образом сгенерировать «секрет клиента» и отправить его через POST в API во время аутентификации. Тогда API будет использовать как свой собственный секрет, так и секрет клиента при формировании JWT. С этим изменением одного JWT будет недостаточно для получения доступа, вам также понадобится секрет клиента.

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

Первоначально опубликовано на michaelries.info.