Dans cette section, nous allons nous concentrer sur les principes de base de l’authentification. Plus précisément, nous allons créer un serveur Ruby (à l’aide de Sinatra) qui implémente le flux web d’une application de différentes manières.
Vous pouvez télécharger le code source complet de ce projet à partir du référentiel platform-samples.
Inscription de votre application
Pour commencer, vous devez inscrire votre application. Chaque OAuth app inscrite se voit attribuer un ID client et à une clé secrète client uniques. La clé secrète client est utilisée pour obtenir un jeton d’accès pour l’utilisateur connecté. Vous devez inclure la clé secrète client dans votre application native, mais les applications web ne doivent pas divulguer cette valeur.
Vous pouvez remplir toutes les informations comme vous le souhaitez, à l’exception de URL de rappel d’autorisation. C’est l’élément le plus important pour configurer de façon sécurisée votre application. Il s’agit de l’URL de rappel vers laquelle GitHub Enterprise Server renvoie l’utilisateur après une authentification réussie. La propriété de cette URL est ce qui garantit que les utilisateurs se connectent à votre application, au lieu de divulguer des jetons à un attaquant.
Étant donné que nous exécutons un serveur Sinatra standard, l’emplacement de l’instance locale est défini sur http://127.0.0.1:4567
. Remplissons l’URL de rappel comme suit : http://127.0.0.1:4567/callback
.
Accepter l’autorisation utilisateur
Avis de désapprobation : GitHub va interrompre le mode d’authentification auprès de l’API à l’aide des paramètres de requête. L’authentification auprès de l’API doit être effectuée au moyen de l’Authentification HTTP de base. Pour plus d’informations, notamment sur les baisses de tension planifiées, consultez le billet de blog.
L’authentification auprès de l’API à l’aide des paramètres de requête, bien que disponible, n’est plus prise en charge pour des raisons de sécurité. Nous recommandons plutôt aux intégrateurs de déplacer leur jeton d’accès, client_id
ou client_secret
dans l’en-tête. GitHub publiera un préavis pour annoncer la suppression de l’authentification au moyen des paramètres de requête.
Maintenant, commençons à remplir notre serveur simple. Créez un fichier appelé server.rb et collez-y ce qui suit :
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
Votre ID client et votre clé secrète client proviennent de la page de configuration de votre application. Nous vous recommandons de stocker ces valeurs en tant que variables d’environnement pour faciliter le remplacement et l’utilisation, ce qui est exactement ce que nous avons fait ici.
Ensuite, dans views/index.erb, collez ce contenu :
<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>
(Si vous n’êtes pas familiarisé avec le fonctionnement de Sinatra, nous vous recommandons de lire le guide Sinatra.)
Notez également que l’URL utilise le paramètre de requête scope
pour définir les étendues demandées par l’application. Pour notre application, nous demandons l’étendue user:email
pour lire les adresses e-mail privées.
Dans votre navigateur, accédez à http://127.0.0.1:4567
. Après avoir cliqué sur le lien, vous devez être redirigé vers GitHub Enterprise Server et accéder à une boîte de dialogue « Autoriser l’application ».
Si vous faites confiance à cette application, cliquez sur Autoriser l’application. Wuh-oh ! Sinatra renvoie une erreur 404
. Quelle en est la raison ?
Vous vous souvenez du moment où nous avons spécifié une URL de rappel sur callback
? Nous n’avons pas fourni d’itinéraire pour celle-ci et dès lors, GitHub Enterprise Server ne sait pas où diriger l’utilisateur après qu'il ait autorisé l'application. Nous allons à présent y remédier.
Mise à disposition d’un rappel
Dans server.rb, ajoutez un itinéraire pour spécifier ce que le rappel doit faire :
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
Après une authentification d’application réussie, GitHub Enterprise Server fournit une valeur code
temporaire.
Vous en aurez besoin pour POST
ce code à GitHub Enterprise Server avec votre code secret en échange d’un access_token
.
Pour simplifier nos requêtes GET et POST HTTP, nous utilisons le client REST.
Notez que vous n’accéderez probablement jamais à l’API via REST. Pour une application plus sérieuse, vous devrez probablement utiliser une bibliothèque écrite dans la langue de votre choix.
Vérification des étendues octroyées
Les utilisateurs peuvent modifier les étendues que vous avez demandées en modifiant directement l’URL. Cela peut octroyer à votre application moins d'accès que ce que vous aviez demandé à l'origine. Avant d’effectuer des requêtes avec le jeton, vérifiez les étendues octroyées pour le jeton par l’utilisateur. Pour plus d’informations sur les étendues demandées et octroyées, consultez « Étendues des applications OAuth ».
Les étendues octroyées sont retournées dans le cadre de la réponse d’échange de jeton.
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
Dans notre application, nous utilisons scopes.include?
pour vérifier si l’étendue user:email
requise pour extraire les adresses e-mail privées de l’utilisateur authentifié nous a été octroyée. Si l'application avait demandé d'autres étendues, nous les aurions également vérifiées.
En outre, étant donné qu’il existe une relation hiérarchique entre les étendues, vous devez vérifier que vous avez reçu le niveau le plus élevé de l’étendue requise. Par exemple, si l’application avait demandé l’étendue user
, elle n’aurait pas reçu explicitement l’étendueuser:email
. Dans ce cas, elle aurait reçu un jeton avec l’étendue user
, qui fonctionne pour demander l’adresse e-mail de l’utilisateur, même s’il n’inclut pas explicitement user:email
sur le jeton. La vérification de user
et de user:email
garantit que vous vérifiez les deux scénarios.
La vérification des étendues avant d’effectuer des requêtes ne suffit pas, car il est possible que les utilisateurs modifient ces étendues entre cette vérification et la requête réelle.
Dans une telle situation, les appels d’API censés aboutir peuvent échouer avec un état 404
ou 401
, ou retourner un autre sous-ensemble d’informations.
Pour vous aider à gérer correctement ces situations, toutes les réponses d’API correspondant aux requêtes effectuées avec des jetons valides comportent également un en-tête X-OAuth-Scopes
.
Cet en-tête contient la liste des étendues du jeton utilisé pour effectuer la requête. En plus de cela, l’API REST fournit un point de terminaison pour vérifier la validité d’un jeton.
Utilisez ces informations pour détecter les modifications apportées aux étendues de jeton et informer vos utilisateurs des modifications apportées aux fonctionnalités d’application disponibles.
Effectuer des requêtes authentifiées
Enfin, ce jeton d’accès vous permet d’effectuer des requêtes authentifiées en tant qu’utilisateur connecté :
# 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
Nous pouvons faire ce que nous voulons avec nos résultats. Dans ce cas, nous allons les placer directement dans 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>
Implémentation d’une authentification « persistante »
Il ne serait pas pratique de demander aux utilisateurs de se connecter à l’application chaque fois qu’ils ont besoin d’accéder à la page web. Par exemple, essayez d’accéder directement à http://127.0.0.1:4567/basic
. Vous obtenez une erreur.
Et si nous contournions l’ensemble du processus « cliquer ici » pour nous souvenir que, tant que l’utilisateur est connecté à GitHub Enterprise Server, il doit être en mesure d’accéder à cette application ? Eh bien, il s’agit précisément de ce que nous allons faire.
Notre petit serveur ci-dessus est plutôt simple. Afin d’intégrer une authentification intelligente, nous allons passer à l'utilisation de sessions pour stocker les jetons. Il en résulte une authentification transparente pour l’utilisateur.
En outre, étant donné que nous conservons les étendues au sein de la session, nous devons gérer les cas où l’utilisateur met à jour les étendues après leur vérification, ou révoque le jeton. Pour ce faire, nous allons utiliser un bloc rescue
et vérifier que le premier appel d’API a réussi, ce qui permet de vérifier que le jeton est toujours valide. Après cela, nous allons vérifier l’en-tête de réponse X-OAuth-Scopes
pour vérifier que l’utilisateur n’a pas révoqué l’étendue user:email
.
Créez un fichier appelé advanced_server.rb, puis collez-y ces lignes :
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
Le code doit globalement vous sembler familier. Par exemple, nous utilisons toujours RestClient.get
pour appeler l’API GitHub Enterprise Server et transmettons nos résultats pour qu’ils s’affichent dans un modèle ERB (appelé cette fois advanced.erb
).
De plus, nous disposons désormais de la méthode authenticated?
qui vérifie si l’utilisateur est déjà authentifié. Si ce n’est pas le cas, la méthode authenticate!
est appelée. Elle effectue le flux OAuth et met à jour la session avec le jeton et les étendues octroyés.
Ensuite, créez un fichier dans les vues appelé advanced.erb et collez-y ce balisage :
<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>
À partir de la ligne de commande, appelez ruby advanced_server.rb
, qui démarre votre serveur sur le port 4567
, le même port que celui utilisé lorsque nous avions une application Sinatra simple.
Lorsque vous accédez à http://127.0.0.1:4567
, l’application appelle authenticate!
qui vous redirige vers /callback
. /callback
nous renvoie à /
, et comme nous avons été authentifiés, affiche avancé.erb.
Nous pourrions simplifier ce routage en remplaçant simplement notre URL de rappel dans GitHub Enterprise Server par /
. Mais server.rb et advanced.rb s’appuyant sur la même URL de rappel, il nous faut en faire un peu plus pour que cela fonctionne.
De plus, si nous n’avions pas autorisé cette application à accéder à nos données GitHub Enterprise Server, nous aurions vu apparaître la même boîte de dialogue de confirmation que précédemment et nous aurions été avertis.