Skip to main content

Authentifizieren bei der REST-API mit einer OAuth-App

Hier erfährst du anhand einiger Beispiele mehr über die verschiedenen Möglichkeiten der Authentifizierung.

In diesem Abschnitt werden die Grundlagen der Authentifizierung beschrieben. Dabei wird (mithilfe von Sinatra) ein Ruby-Server erstellt, der den Webflow einer Anwendung auf unterschiedliche Weise implementiert.

Du kannst den vollständigen Quellcode für dieses Projekt aus dem Repository mit den Plattformbeispielen herunterladen.

Registrieren deiner App

Zunächst musst du Deine Anwendung registrieren. Jeder registrierten OAuth app werden eine eindeutige Client-ID und ein geheimer Clientschlüssel zugewiesen. Der geheime Clientschlüssel wird verwendet, um ein Zugriffstoken für den angemeldeten Benutzer abzurufen. Du musst den geheimen Clientschlüssel in deine native Anwendung einschließen. Bei Webanwendungen darf dieser Wert jedoch nicht in unbefugten Besitz kommen.

Dabei kannst du mit Ausnahme der Rückruf-URL für die Autorisierung sämtliche anderen Informationen eintragen. Dies ist das wichtigste Element bei der sicheren Einrichtung deiner Anwendung. GitHub leitet Benutzer*innen nach der erfolgreichen Authentifizierung an diese Rückruf-URL. Der Besitz dieser URL stellt sicher, dass sich Benutzer bei deiner App anmelden, anstatt dass Token in die Hände eines Angreifers geraten.

Da wir einen regulären Sinatra-Server ausführen, wird der Speicherort der lokalen Instanz auf http://127.0.0.1:4567 festgelegt. Hier wird http://127.0.0.1:4567/callback als Rückruf-URL angegeben.

Akzeptieren der Benutzerautorisierung

Veraltungshinweis: GitHub führt die Authentifizierung mit der API mit Abfrageparametern nicht mehr aus. Die Authentifizierung mit der API sollte mit HTTP-Standardauthentifizierung erfolgen. Die Verwendung von Abfrageparametern zur Authentifizierung bei der API funktioniert am 5. Mai 2021 nicht mehr. Weitere Informationen (einschließlich geplanter Änderungen) findest du im Blogbeitrag.

Im nächsten Schritt werden die Serverinformationen angegeben. Erstelle eine Datei server.rb, und füge die folgenden Angaben in diese Datei ein:

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

Deine Client-ID und dein geheimer Clientschlüssel stammen von der Konfigurationsseite deiner Anwendung. Du solltest diese Werte als Umgebungsvariablen speichern, um den Austausch und die Verwendung zu erleichtern. Genau das haben wir hier getan.

Füge nun in views/index.erb diesen Inhalt ein:

<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>

(Wenn du nicht mit der Funktionsweise von Sinatra vertraut bist, solltest du den Sinatra-Leitfaden lesen.)

Beachte außerdem, dass die URL den scope-Abfrageparameter verwendet, um die von der Anwendung angeforderten Bereiche zu definieren. Für diese Anwendung wird der Bereich user:email zum Lesen privater E-Mail-Adressen angefordert.

Navigiere in deinem Browser zu http://127.0.0.1:4567. Wenn du auf den Link klickst, solltest du zu GitHub geleitet werden. Dort wird ein Dialogfeld „Anwendung autorisieren“ angezeigt.

Wenn du dir selbst vertraust, klicke auf App autorisieren. Oje! Sinatra gibt einen 404-Fehler aus. Was ist los?

Erinnerst du dich, dass wir als Rückruf-URL callback angegeben haben? Dabei haben wir keine entsprechende Route festgelegt. GitHub weiß also nicht, wohin Benutzer*innen geleitet werden sollen, nachdem sie die App autorisiert haben. Im Folgenden wird dieses Problem behoben.

Angeben eines Rückrufwerts

Füge in server.rb eine Route für den Rückruf hinzu:

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

Nach einer erfolgreichen App-Authentifizierung stellt GitHub einen temporären code-Wert bereit. Diesen Code musst du per POST mit deinem geheimen Clientschlüssel im Austausch für ein access_token an GitHub zurückgeben. Zur Vereinfachung dieser GET- und POST-HTTP-Anforderungen verwenden wir rest-client. Beachte, dass du wahrscheinlich niemals über REST auf die API zugreifen wirst. In der Praxis solltest du vermutlich eher eine Bibliothek in deiner bevorzugten Sprache verwenden.

Überprüfen der gewährten Bereiche

Benutzerinnen können die angeforderten Bereiche bearbeiten, indem sie die URL direkt ändern. Dadurch kann deine Anwendung einen geringeren Umfang an Zugriffsberechtigungen erhalten, als du ursprünglich angefordert hattest. Bevor du Anforderungen mit dem Token erstellst, solltest du überprüfen, welche Bereiche der oder dem Benutzerin für das Token gewährt wurden. Weitere Informationen zu angeforderten und gewährten Bereichen findest du unter Bereiche für OAuth-Apps.

Die gewährten Bereiche werden beim Austausch eines Tokens als Teil der Antwort zurückgegeben.

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

In unserer Anwendung wird mithilfe von scopes.include? überprüft, ob der Bereich user:email gewährt wurde, der zum Abrufen der privaten E-Mail-Adressen der authentifizierten Benutzer*innen erforderlich ist. Wenn die Anwendung weitere Bereiche angefordert hätte, hätten wir auch diese Bereiche überprüft.

Da zudem eine hierarchische Beziehung zwischen Bereichen besteht, solltest du überprüfen, ob dir höhere Stufen des erforderlichen Bereichs gewährt wurden. Wenn die Anwendung z. B. den Bereich user angefordert hat, wurde ihr möglicherweise der Bereich user:email nicht explizit zugewiesen. In diesem Fall erhält sie ein Token mit dem Bereich user. Dies würde zum Anfordern der Benutzer-E-Mail-Adresse funktionieren, obwohl user:email nicht explizit in das Token eingeschlossen wird. Die Überprüfung auf user und user:email stellt sicher, dass du für beide Szenarien prüfst.

Da Benutzer*innen die Bereiche zwischen deiner Überprüfung und der eigentlichen Anforderung noch ändern können, ist es nicht ausreichend, die Bereiche lediglich vor dem Ausführen von Anforderungen zu überprüfen. Denn bei einer solchen Änderung tritt bei API-Aufrufen, bei denen du eine erfolgreiche Ausführung erwartet hast, möglicherweise ein Fehler mit dem Status 404 bzw. 401 auf, oder du erhältst eine andere Teilmenge an Informationen als erwartet.

Damit diese Situation vermieden wird, enthalten alle API-Antworten auf Anforderungen mit gültigen OAuth-App-Token auch einen X-OAuth-Scopes-Header. In diesem Header sind die Bereiche des Tokens aufgeführt, das für die Anforderung verwendet wurde. Darüber hinaus stellt die REST-API einen Endpunkt bereit, um ein Token auf Gültigkeit zu überprüfen. Verwende diese Informationen, um Änderungen an Tokenbereichen zu ermitteln, und informiere deine Benutzer*innen über Änderungen bei den verfügbaren Anwendungsfunktionen.

Ausführen von authentifizierten Anforderungen

Schließlich kannst du mit diesem Zugriffstoken authentifizierte Anforderungen als angemeldeter Benutzerin vornehmen:

# 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

Mit den Ergebnissen kannst du beliebige Schritte ausführen. In diesem Fall setzen wir unsere Arbeit direkt mit basic.erb fort:

<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>

Implementieren einer „persistenten“ Authentifizierung

Es wäre sehr ungünstig, wenn Benutzer*innen sich jedes Mal bei der App anmelden müssten, wenn sie auf die Webseite zugreifen möchten. Versuche z. B., direkt zu http://127.0.0.1:4567/basic zu navigieren. Du erhältst eine Fehlermeldung.

Was wäre, wenn der gesamte „Hier klicken“-Vorgang umgangen werden könnte? Und stattdessen einfach festgelegt_ _wird, dass Benutzer*innen auf die Anwendung zugreifen können sollen, solange sie bei GitHub angemeldet sind? Genau das werden wir jetzt umsetzen.

Unser kleiner Server oben ist ein ziemlich einfaches Beispiel. Für einen intelligenten Authentifizierungsprozess verwenden wir nun Sitzungen, um Token zu speichern. Dadurch wird die Authentifizierung für Benutzer*innen transparent.

Da die Bereiche innerhalb der Sitzung beibehalten werden, muss zudem eine Vorgehensweise für Situationen definiert werden, in denen Benutzerinnen die Bereiche nach der Überprüfung aktualisieren oder das Token widerrufen. Dazu wird ein rescue-Block verwendet, und es wird überprüft, ob der erste API-Aufruf erfolgreich ist. Dadurch wird sichergestellt, dass das Token noch gültig ist. Danach wird anhand des X-OAuth-Scopes-Antwortheaders überprüft, ob der user:email-Bereich durch die oder den Benutzerin widerrufen wurde.

Erstelle eine Datei advanced_server.rb, und füge diese Zeilen in der Datei ein:

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

Der Code sollte Ihnen im Wesentlichen vertraut erscheinen. Es wird z. B. weiterhin RestClient.get für den Aufruf der GitHub-API verwendet, und die Ergebnisse werden wie zuvor für die Anzeige in einer ERB-Vorlage (in diesem Fall advanced.erb) übergeben.

Außerdem wird nun mithilfe der authenticated?-Methode überprüft, ob die oder der Benutzer*in bereits authentifiziert ist. Falls nicht, wird die Methode authenticate! aufgerufen, mit der der OAuth-Flow ausgeführt und die Sitzung mit den gewährten Bereichen und Token aktualisiert wird.

Als Nächstes erstellst du in views die Datei advanced.erb und fügst dieses Markup ein:

<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>

Rufe an der Befehlszeile ruby advanced_server.rb auf, um deinen Server an Port 4567 zu starten. Dies ist derselbe Port, den wir bereits bei der einfachen Sinatra-App verwendet haben. Wenn du zu http://127.0.0.1:4567 navigierst, ruft die App authenticate! auf, um dich zu /callback umzuleiten. /callback leitet dich dann erneut zu / und zeigt advanced.erb an, da du bereits authentifiziert wurdest.

Dieses Roundtriprouting könnte erheblich vereinfacht werden, indem die Rückruf-URL in GitHub ganz einfach in / geändert wird. Da server.rb und advanced.rb dieselbe Rückruf-URL verwenden, sind diese zusätzlichen Schritte jedoch erforderlich.

Wäre diese Anwendung zudem niemals für den Zugriff auf die GitHub-Daten autorisiert worden, wäre als Warnhinweis dasselbe Bestätigungsdialogfeld angezeigt worden wie zuvor.