Skip to main content

Проверка подлинности в REST API с помощью приложения OAuth

Изучите несколько примеров, демонстрирующих различные способы проверки подлинности.

Этот раздел посвящен основам проверки подлинности. В частности, мы создадим сервер Ruby (с помощью Sinatra), который реализует веб-поток приложения несколькими разными способами.

Полный исходный код для этого проекта можно скачать из репозитория platform-samples.

Регистрация приложения

Сначала нужно зарегистрировать приложение. Каждый зарегистрированный OAuth app назначается уникальный идентификатор клиента и секрет клиента. Секрет клиента используется для получения маркера доступа для вошедшего пользователя. Необходимо включить секрет клиента в собственное приложение, однако веб-приложения не должны утечки этого значения.

Вы можете заполнить все остальные сведения, однако вам нравится, кроме URL-адреса обратного вызова авторизации. Это самый важный элемент для безопасной настройки приложения. Это URL-адрес обратного вызова, на который GitHub Enterprise Cloud возвращает пользователя после успешной проверки подлинности. Владение этим URL-адресом — это обеспечение входа пользователей в приложение, а не утечки маркеров злоумышленнику.

Так как мы запускаем обычный сервер Sinatra, в качестве расположения локального экземпляра задается http://127.0.0.1:4567. Давайте заполним URL-адрес обратного вызова значением http://127.0.0.1:4567/callback.

Подтверждение авторизации пользователя

Уведомление об устаревании. GitHub прекратит проверку подлинности в API с использованием параметров запроса. Проверка подлинности в API должна выполняться с использованием обычной проверки подлинности HTTP. Использование параметров запросов для проверки подлинности в API не будет работать с 5 мая 2021 г. Дополнительные сведения, включая плановые ограничения нагрузки, см. в этой записи блога.

Теперь давайте приступим к настройке нашего простого сервера. Создайте файл с именем server.rb и вставьте в него следующий код:

require 'sinatra'
require 'rest-client'
require 'json'

CLIENT_ID = ENV['GH_BASIC_CLIENT_ID']
CLIENT_SECRET = ENV['GH_BASIC_SECRET_ID']

get '/' do
  erb :index, :locals => {:client_id => CLIENT_ID}
end

Идентификатор клиента и секрет клиента поступают на странице конфигурации приложения. Мы рекомендуем хранить эти значения в качестве переменных среды для упрощения замены и использования. Это именно то, что мы сделали здесь.

Далее вставьте в файл views/index.erb следующее содержимое:

<html>
  <head>
  </head>
  <body>
    <p>
      Well, hello there!
    </p>
    <p>
      We're going to now talk to the GitHub API. Ready?
      <a href="https://github.com/login/oauth/authorize?scope=user:email&client_id=<%= client_id %>">Click here</a> to begin!
    </p>
    <p>
      If that link doesn't work, remember to provide your own <a href="/apps/building-oauth-apps/authorizing-oauth-apps/">Client ID</a>!
    </p>
  </body>
</html>

(Если вы не знакомы с тем, как работает Sinatra, рекомендуем ознакомиться с руководством по Sinatra.)

Кроме того, обратите внимание, что в URL-адресе используется параметр запроса scope для определения областей, запрашиваемых приложением. Для нашего приложения мы запрашиваем область user:email для чтения частных адресов электронной почты.

В браузере перейдите по адресу http://127.0.0.1:4567. Щелкнув ссылку, необходимо перейти к GitHub Enterprise Cloud, а затем открыть диалоговое окно "Авторизовать приложение".

Если вы доверяете себе, нажмите кнопку Авторизовать приложение. Ой! Sinatra выдает ошибку 404. Что же случилось?!

Помните, мы указали callback в качестве URL-адреса обратного вызова? Мы не предоставили маршрут для него, поэтому GitHub Enterprise Cloud не знает, куда направлять пользователя после авторизации приложения. Давайте исправим это.

Предоставление обратного вызова

В файле server.rb добавьте маршрут, чтобы указать, что должен делать обратный вызов:

get '/callback' do
  # get temporary GitHub code...
  session_code = request.env['rack.request.query_hash']['code']

  # ... and POST it back to GitHub
  result = RestClient.post('https://github.com/login/oauth/access_token',
                          {:client_id => CLIENT_ID,
                           :client_secret => CLIENT_SECRET,
                           :code => session_code},
                           :accept => :json)

  # extract the token and granted scopes
  access_token = JSON.parse(result)['access_token']
end

После успешной проверки подлинности приложения GitHub Enterprise Cloud предоставляет временное значение code. Этот код потребуется POST вернуть к GitHub Enterprise Cloud с секретом клиента в обмен на access_token. Чтобы упростить HTTP-запросы GET и POST, мы используем rest-client. Обратите внимание, что вы, вероятно, никогда не будете обращаться к API через REST. Для более серьезного приложения, вероятно, следует использовать библиотеку, написанную на выбранном вами языке.

Проверка предоставленных областей

Пользователи могут изменять запрошенные области, изменяя URL-адрес напрямую. Таким образом приложению может предоставляться более ограниченный доступ, чем вы запросили изначально. Перед выполнением запросов с маркером проверка область, предоставленных для маркера пользователем. Дополнительные сведения о запрошенных и предоставленных область см. в разделе "Области для приложений OAuth".

Предоставленные области возвращаются в ответе обмена токеном.

get '/callback' do
  # ...
  # Get the access_token using the code sample above
  # ...

  # check if we were granted user:email scope
  scopes = JSON.parse(result)['scope'].split(',')
  has_user_email_scope = scopes.include? 'user:email' || scopes.include? 'user'
end

В нашем приложении мы используем scopes.include? для проверки того, была ли предоставлена область user:email, необходимая для получения частных адресов электронной почты пользователя, прошедшего проверку подлинности. Если бы приложение запрашивало другие области, мы бы также проверили их.

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

Проверки областей только перед выполнением запросов недостаточно, так как пользователи могут изменять области в период между проверкой и фактическим запросом. В этом случае вызовы API, которые должны выполняться успешно, могут завершаться ошибкой с состоянием 404 или 401 или возвращать другие сведения.

Чтобы упростить обработку этих ситуаций, все ответы API на запросы, выполненные с допустимыми маркерами приложения OAuth, также содержат X-OAuth-Scopes заголовок. Этот заголовок содержит список областей токена, который использовался для выполнения запроса. Кроме того, REST API предоставляет конечную точку для проверка маркера для допустимости. Используйте эти сведения для обнаружения изменений в областях токена и информирования пользователей об изменениях в доступных функциональных возможностях приложения.

Выполнение запросов с проверкой подлинности

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

# fetch user information
auth_result = JSON.parse(RestClient.get('https://api.github.com/user',
                                        {:params => {:access_token => access_token}}))

# if the user authorized it, fetch private emails
if has_user_email_scope
  auth_result['private_emails'] =
    JSON.parse(RestClient.get('https://api.github.com/user/emails',
                              {:params => {:access_token => access_token}}))
end

erb :basic, :locals => auth_result

С результатами можно делать что угодно. В этом случае мы просто сбросим их в файл basic.erb:

<p>Hello, <%= login %>!</p>
<p>
  <% if !email.nil? && !email.empty? %> It looks like your public email address is <%= email %>.
  <% else %> It looks like you don't have a public email. That's cool.
  <% end %>
</p>
<p>
  <% if defined? private_emails %>
  With your permission, we were also able to dig up your private email addresses:
  <%= private_emails.map{ |private_email_address| private_email_address["email"] }.join(', ') %>
  <% else %>
  Also, you're a bit secretive about your private email addresses.
  <% end %>
</p>

Реализация сохраняемой проверки подлинности

Если бы пользователям требовалось входить в приложение каждый раз, когда им нужно посетить веб-страницу, это было бы неудобно. Например, попробуйте перейти непосредственно по адресу http://127.0.0.1:4567/basic. Вы получите ошибку.

Что если бы мы могли обойти весь ручной процесс и сделать так, чтобы пользователь, вошедший на GitHub Enterprise Cloud, мог автоматически получать доступ к приложению? Приготовься, ведь именно это мы собираемся сделать.

Описанный выше сервер довольно прост. Чтобы добавить интеллектуальные возможности проверки подлинности, мы перейдем на использование сеансов для хранения токенов. Это сделает проверку подлинности прозрачной для пользователя.

Кроме того, так как области хранятся в сеансе, нам потребуется обрабатывать случаи, когда пользователь изменяет области после их проверки или отзывает токен. Для этого мы используем блок rescue и проверим, выполнен ли первый вызов API успешно и, следовательно, действителен ли токен. После этого мы проверим заголовок ответа X-OAuth-Scopes, чтобы убедиться в том, что пользователь не отозвал область user:email.

Создайте файл с именем advanced_server.rb и вставьте в него следующие строки:

require 'sinatra'
require 'rest_client'
require 'json'

# Don't use hard-coded values in your app
# Instead, set and test environment variables, like below
# if ENV['GITHUB_CLIENT_ID'] && ENV['GITHUB_CLIENT_SECRET']
#  CLIENT_ID        = ENV['GITHUB_CLIENT_ID']
#  CLIENT_SECRET    = ENV['GITHUB_CLIENT_SECRET']
# end

CLIENT_ID = ENV['GH_BASIC_CLIENT_ID']
CLIENT_SECRET = ENV['GH_BASIC_SECRET_ID']

use Rack::Session::Pool, :cookie_only => false

def authenticated?
  session[:access_token]
end

def authenticate!
  erb :index, :locals => {:client_id => CLIENT_ID}
end

get '/' do
  if !authenticated?
    authenticate!
  else
    access_token = session[:access_token]
    scopes = []

    begin
      auth_result = RestClient.get('https://api.github.com/user',
                                   {:params => {:access_token => access_token},
                                    :accept => :json})
    rescue => e
      # request didn't succeed because the token was revoked so we
      # invalidate the token stored in the session and render the
      # index page so that the user can start the OAuth flow again

      session[:access_token] = nil
      return authenticate!
    end

    # the request succeeded, so we check the list of current scopes
    if auth_result.headers.include? :x_oauth_scopes
      scopes = auth_result.headers[:x_oauth_scopes].split(', ')
    end

    auth_result = JSON.parse(auth_result)

    if scopes.include? 'user:email'
      auth_result['private_emails'] =
        JSON.parse(RestClient.get('https://api.github.com/user/emails',
                       {:params => {:access_token => access_token},
                        :accept => :json}))
    end

    erb :advanced, :locals => auth_result
  end
end

get '/callback' do
  session_code = request.env['rack.request.query_hash']['code']

  result = RestClient.post('https://github.com/login/oauth/access_token',
                          {:client_id => CLIENT_ID,
                           :client_secret => CLIENT_SECRET,
                           :code => session_code},
                           :accept => :json)

  session[:access_token] = JSON.parse(result)['access_token']

  redirect '/'
end

Большая часть кода должна выглядеть знакомо. Например, мы по-прежнему используем RestClient.get для вызова API GitHub и по-прежнему передаем результаты для обработки в шаблоне ERB (на этот раз он называется advanced.erb).

Кроме того, теперь у нас есть метод authenticated?, который проверяет, прошел ли пользователь проверку подлинности. Если нет, вызывается метод authenticate!, который выполняет поток OAuth и обновляет сеанс с учетом предоставленных токена и областей.

Затем создайте в папке views файл с именем advanced.erb и вставьте в него следующую разметку:

<html>
  <head>
  </head>
  <body>
    <p>Well, well, well, <%= login %>!</p>
    <p>
      <% if !email.empty? %> It looks like your public email address is <%= email %>.
      <% else %> It looks like you don't have a public email. That's cool.
      <% end %>
    </p>
    <p>
      <% if defined? private_emails %>
      With your permission, we were also able to dig up your private email addresses:
      <%= private_emails.map{ |private_email_address| private_email_address["email"] }.join(', ') %>
      <% else %>
      Also, you're a bit secretive about your private email addresses.
      <% end %>
    </p>
  </body>
</html>

В командной строке вызовите ruby advanced_server.rb, чтобы запустить сервер через порт 4567 — тот же порт, который мы использовали с простым приложением Sinatra. При переходе по адресу http://127.0.0.1:4567 приложение вызывает authenticate!, в результате чего вы перенаправляетесь на /callback. Затем /callback отправляет нас обратно на /, и так как мы прошли проверку подлинности, отрисовывается содержимое файла advanced.erb.

Мы могли бы сильно упростить эту циклическую маршрутизацию, просто изменив URL-адрес обратного вызова в GitHub Enterprise Cloud на /. Но, так как и server.rb, и advanced.rb используют один и тот же URL-адрес обратного вызова, для этого необходимы дополнительные действия.

Кроме того, если бы мы никогда прежде не разрешали приложению доступ к данным GitHub Enterprise Cloud, появилось бы то же диалоговое окно с предупреждением, что и ранее.