Skip to main content

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

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

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

Tip

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

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

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

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

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

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

Warning

Sunset Обратите внимание: проверка подлинности в API GitHub больше не доступна с помощью параметров запроса. Аутентификация в API должна выполняться с помощью базовой проверки подлинности HTTP. Дополнительные сведения, включая запланированные браунуты, см. в записи блога. аутентификацию в API с помощью параметров запроса в то время как они больше не поддерживаются из-за проблем безопасности. Вместо нее рекомендуем интеграторам переместить токен доступа client_id или client_secret в заголовок. GitHub объявит об удалении проверки подлинности с использованием параметров запросов предварительным уведомлением.

Теперь давайте приступим к настройке нашего простого сервера. Создайте файл с именем 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 Server, а затем открыть диалоговое окно "Авторизовать приложение".

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

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

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

В файле 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 Server предоставляет временное значение code. Этот код потребуется POST вернуть к GitHub Enterprise Server с секретом клиента в обмен на 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('http(s)://HOSTNAME/api/v3/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('http(s)://HOSTNAME/api/v3/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 Server, мог автоматически получать доступ к приложению? Приготовься, ведь именно это мы собираемся сделать.

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

Кроме того, так как области хранятся в сеансе, нам потребуется обрабатывать случаи, когда пользователь изменяет области после их проверки или отзывает токен. Для этого мы используем блок 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('http(s)://HOSTNAME/api/v3/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('http(s)://HOSTNAME/api/v3/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 Enterprise Server и по-прежнему передаем результаты для обработки в шаблоне 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 Server на /. Но, так как и server.rb, и advanced.rb используют один и тот же URL-адрес обратного вызова, для этого необходимы дополнительные действия.

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