Skip to main content

Эта версия GitHub Enterprise Server будет прекращена 2026-08-25. Снятые релизы не поддерживаются. Исправления выпускаться не будут даже при критических проблемах безопасности. Для лучшей производительности, повышения безопасности и новых функций GitHub Enterprise Server см. Обзор процесса обновления. Для помощи с обновлением обращайтесь в GitHub Enterprise Support.

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

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

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

Совет

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

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

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

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

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

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

Предупреждение

** На пенсии Уведомление:** Аутентификация в GitHub API больше недоступен через параметры запроса. Аутентификация в 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+offline_access&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 для чтения частных адресов электронной почты. Мы также запросить offline_access область получения маркеров истечения срока действия, которые лучше подходят для безопасности.

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

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

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

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

В файле 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 приложения предоставляет временное code значение. Этот код потребуется POST вернуться GitHub к секрету клиента в обмен на 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систему, они должны иметь доступ к этому приложению? Приготовься, ведь именно это мы собираемся сделать.

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

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

Маркеры доступа также могут истекать, если приложение настроит запрос на короткие маркеры. При обмене временным codeответом может быть включен refresh_token рядом с access_tokenним. Мы также сохраняем маркер обновления в сеансе, и если вызов API завершается сбоем, так как срок действия маркера доступа истек или был отменен, мы будем использовать маркер обновления для запроса нового маркера доступа и повтора запроса. Только если обновление также завершается ошибкой, мы снова запускаем поток OAuth.

Создайте файл с именем 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

def refresh_access_token!
  # exchange the stored refresh token for a new access token
  result = RestClient.post('https://github.com/login/oauth/access_token',
                          {:client_id => CLIENT_ID,
                           :client_secret => CLIENT_SECRET,
                           :grant_type => 'refresh_token',
                           :refresh_token => session[:refresh_token]},
                           :accept => :json)

  parsed_result = JSON.parse(result)
  new_access_token = parsed_result['access_token']

  # if we didn't get a new access token back, the refresh failed
  return false unless new_access_token

  # store the new access token and refresh token in the session
  session[:access_token] = new_access_token
  session[:refresh_token] = parsed_result['refresh_token']
  true
rescue
  false
end

get '/' do
  if !authenticated?
    authenticate!
  else
    scopes = []
    refreshed = false

    begin
      access_token = session[:access_token]
      auth_result = RestClient.get('http(s)://HOSTNAME/api/v3/user',
                                   {:params => {:access_token => access_token},
                                    :accept => :json})
    rescue => e
      # the request didn't succeed because the token was revoked or has
      # expired. If we haven't already tried and we have a refresh token,
      # get a new access token and retry the request once
      if !refreshed && session[:refresh_token] && refresh_access_token!
        refreshed = true
        retry
      end

      # we couldn't refresh the token, so we invalidate the tokens stored in
      # the session and render the index page so that the user can start the
      # OAuth flow again
      session[:access_token] = nil
      session[:refresh_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)

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

  redirect '/'
end

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

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

Метод refresh_access_token! обменивается refresh_token сохраненным в сеансе новым access_token. При сбое вызова API в rescue блоке мы вызываем этот метод и используем ключевое слово Ruby retry для повторного выполнения запроса с новым маркером. Если обновление завершается ошибкой, так как срок действия маркера обновления также истек или маркер был отозван, мы очищаем сеанс и отправляем пользователя обратно через поток OAuth. Чтобы предотвратить бесконечный цикл проверки подлинности, мы также помните, что мы обновили маркер, поэтому, если API снова завершается сбоем, это может быть связано с тем, что пользователь потерял доступ к ресурсу. Если это так, то никакое обновление маркера не исправит вызов API.

Затем создайте в папке 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/. Но, так как и server.rb, и advanced.rb используют один и тот же URL-адрес обратного вызова, для этого необходимы дополнительные действия.

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