TutorielGuides

Connecter un appareil (device flow)

Sur cette page

Un téléviseur, un terminal ou un objet connecté n’a ni clavier confortable ni navigateur pour afficher la page de connexion de TOSIAM. Le device flow (autorisation d’appareil) contourne le problème : l’appareil affiche un code court et une adresse ; l’utilisateur ouvre l’adresse sur son téléphone ou son ordinateur, se connecte, saisit le code et accepte ; pendant ce temps, l’appareil interroge TOSIAM jusqu’à recevoir ses jetons. L’appareil ne voit jamais le mot de passe.

Le code est dans le dépôt tosiam-samples, dossier tosiam-device-flow : un appareil en ligne de commande, Node.js sans dépendance.

  1. Appareil vers TOSIAM Demande un code sur /oauth2/ref/device/code
  2. TOSIAM vers Appareil Renvoie device_code, user_code, verification_url, interval et expires_in
  3. Appareil Affiche l’adresse et le code
  4. Utilisateur (téléphone) vers TOSIAM Ouvre l’adresse, se connecte, saisit le code
  5. TOSIAM vers Utilisateur (téléphone) Demande le consentement
  6. Appareil vers TOSIAM Interroge /oauth2/ref/access_token toutes les 5 s (code=device_code)
  7. TOSIAM vers Appareil Répond authorization_pending
  8. Utilisateur (téléphone) vers TOSIAM Autorise l’appareil
  9. TOSIAM vers Appareil Renvoie l’access token et le refresh token
  10. Appareil vers TOSIAM Lit le profil sur userinfo, puis révoque ses jetons
Le device flow : l’appareil affiche le code et interroge TOSIAM pendant que l’utilisateur valide sur son téléphone.

Étape 1 : le client de l’appareil#

L’appareil est un client public : distribué à des milliers d’exemplaires, il ne peut pas garder de secret. Dans Clients, Créer un agent ouvre l’assistant :

  • étape Identité : l’identifiant appareil-tv, sans URI de redirection (l’appareil n’en reçoit pas) ;
  • étape Sécurité : type de client Public, qui ne demande pas de secret et fixe la méthode d’authentification à none ; dans les types d’autorisation, cochez Device Code et Refresh Token et décochez les autres ; scope profile seulement.

Créez le client, puis vérifiez sa fiche. Onglet Général :

Fiche du client appareil-tv, onglet Général : type de client Public, scope profile Fiche du client appareil-tv, onglet Général : type de client Public, scope profile
Type de client Public (1) et scope profile (2).

Le Nom du client (Appareil TV dans l’exemple) est le nom que l’utilisateur lira sur l’écran de consentement : choisissez-le pour qu’il reconnaisse son appareil.

Onglet Avancé, en haut :

Fiche du client appareil-tv, onglet Avancé : type de réponse token coché, méthode d'authentification aucune Fiche du client appareil-tv, onglet Avancé : type de réponse token coché, méthode d'authentification aucune
Type de réponse token (1) et méthode d’authentification aucune (2).
  • Types de réponse : token. L’assistant ne le propose pas ; or device/code exige un response_type, qui doit figurer ici. Sans lui, l’utilisateur tombe sur unsupported_response_type après avoir saisi le code.
  • Méthode d’authentification au point d’accès des jetons : aucune (none), celle des clients publics.

Plus bas, dans le même onglet :

Fiche du client appareil-tv, onglet Avancé : consentement implicite désactivé, types d'autorisation Device Code et Refresh Token cochés Fiche du client appareil-tv, onglet Avancé : consentement implicite désactivé, types d'autorisation Device Code et Refresh Token cochés
Consentement implicite désactivé (1), types d’autorisation Device Code (2) et Refresh Token (3).
  • Consentement implicite : désactivé. L’utilisateur doit voir quel appareil il autorise, et quoi.
  • Types d’autorisation : Device Code (http://oauth.net/grant_type/device/1.0) et Refresh Token.

Pour un client public, l’assistant annonce PKCE obligatoire : ce réglage, s’il est activé, ne gêne pas le device flow.

Étape 2 : demander un code#

L’appareil commence par demander un code à device/code. client_id, scope et response_type sont obligatoires. Le type de réponse doit figurer dans la liste du client, et les scopes dans ses scopes : TOSIAM ne le vérifie pas à cette étape, l’utilisateur tomberait sur unsupported_response_type ou invalid_scope après avoir saisi le code.

javascript· appareil/tosiam.js
async requestCode(scope) {
  const { response, data, text } = await call('/device/code',
    { form: { client_id: clientId, scope, response_type: 'token' } });
  if (!response.ok) throw errorFor(response.status, data, text);
  if (!data.device_code || !data.user_code || !data.verification_url || !Number.isFinite(data.expires_in)) {
    throw unexpected();   // une page HTML ou autre chose que TOSIAM : TOSIAM_URL à vérifier
  }
  return { deviceCode: data.device_code, userCode: data.user_code, interval: data.interval,
    expiresIn: data.expires_in, verificationUrl: data.verification_url };
},

TOSIAM répond :

json
{
  "device_code": "9b3e2f4a-…",
  "user_code": "hc6FLmLQ",
  "verification_url": "http://localhost:8080/tosiam/oauth2/ref/device/user",
  "interval": 5,
  "expires_in": 300
}
  • device_code : le secret de l’appareil, qu’il présentera pour obtenir les jetons. Il ne s’affiche pas.
  • user_code : le code que l’utilisateur recopie. La page l’accepte sans tenir compte de la casse : l’exemple l’affiche en majuscules, plus lisibles à l’écran et plus faciles à taper sur un téléphone.
  • verification_url : la page du royaume du client, où l’utilisateur saisit le code.
  • interval et expires_in : l’appareil attend 5 secondes entre deux appels, et le code expire au bout de 5 minutes. Les deux se règlent dans le fournisseur OAuth2 (« Intervalle d’interrogation des appareils », « Durée de vie du code d’appareil (secondes) »).

Étape 3 : afficher le code#

javascript· appareil/appareil.js
const code = await client.requestCode(values.scope);
const userCode = code.userCode.toUpperCase();
out(`Sur votre téléphone ou votre ordinateur, ouvrez : ${code.verificationUrl}`);
out(`et saisissez le code : ${userCode}`);
out(`Lien direct : ${code.verificationUrl}?user_code=${encodeURIComponent(userCode)}`);

Le lien direct contient déjà le code : l’utilisateur n’a plus qu’à se connecter et accepter. C’est ce qu’un vrai appareil affiche en QR code, à côté du code pour qui préfère le saisir.

Étape 4 : attendre l’utilisateur#

L’appareil interroge ensuite l’endpoint de jeton avec le grant de l’appareil et son device_code, dans le paramètre code :

javascript· appareil/tosiam.js
async poll(deviceCode) {
  const { response, data, text } = await call('/access_token',
    { form: { client_id: clientId, grant_type: DEVICE_GRANT, code: deviceCode } });
  if (response.ok) {
    if (!data.access_token) throw unexpected();
    return { tokens: { accessToken: data.access_token, refreshToken: data.refresh_token,
      expiresIn: data.expires_in, scope: data.scope } };
  }
  if (data?.error === 'authorization_pending') return { pending: true };
  if (data?.error === 'slow_down') return { slowDown: true };
  throw errorFor(response.status, data, text);
},
Faites défiler le tableau
Réponse de TOSIAMSignificationCe que fait l’appareil
authorization_pendingL’utilisateur n’a pas encore validéAttend l’intervalle et recommence
slow_downL’appareil interroge trop viteAjoute 5 secondes à l’intervalle, pour de bon
200 avec access_token et refresh_tokenL’utilisateur a acceptéPasse à l’étape 6
authorization_declinedL’utilisateur a refusé (ou le code a déjà servi)S’arrête (code de sortie 1)
expired_tokenLe code a expiréS’arrête et propose de relancer (code de sortie 1)

La boucle attend avant chaque appel et s’arrête d’elle-même à l’expiration du code, même si TOSIAM répond encore authorization_pending :

javascript· appareil/tosiam.js
export async function waitForTokens(client, code, { sleep = ms => new Promise(r => setTimeout(r, ms)),
  now = Date.now, onWait } = {}) {
  const deadline = now() + code.expiresIn * 1000;
  let interval = code.interval >= 1 ? code.interval * 1000 : DEFAULT_INTERVAL;
  for (;;) {
    await sleep(interval);
    if (now() >= deadline) throw new TosiamError('expired', 'Le code a expiré');
    const result = await client.poll(code.deviceCode);
    if (result.tokens) return result.tokens;
    if (result.slowDown) interval += SLOW_DOWN_STEP;
    else onWait?.(Math.round((deadline - now()) / 1000));
  }
}

Étape 5 : côté utilisateur#

L’utilisateur ouvre l’adresse affichée par l’appareil, saisit le code (1) et valide (2) :

Page de saisie du code de TOSIAM : champ du code rempli avec HC6FLMLQ et bouton Submit Page de saisie du code de TOSIAM : champ du code rempli avec HC6FLMLQ et bouton Submit
Le code de l’appareil (1) et le bouton de validation (2). Cette page n’est pas encore traduite en français.

S’il n’est pas connecté, TOSIAM lui présente sa page de connexion, puis le ramène au code :

Page de connexion de TOSIAM : nom d'utilisateur et mot de passe Page de connexion de TOSIAM : nom d'utilisateur et mot de passe
La connexion se fait sur TOSIAM, jamais sur l’appareil.

L’écran de consentement nomme l’appareil (1) et ce qu’il demande (2) ; l’utilisateur refuse (3) ou autorise (4) :

Écran de consentement : application Appareil TV, information demandée Profil, connecté en tant que Donald, boutons Refuser et Autoriser Écran de consentement : application Appareil TV, information demandée Profil, connecté en tant que Donald, boutons Refuser et Autoriser
Le nom du client (1), le scope demandé (2), Refuser (3) et Autoriser (4).

Enfin, TOSIAM confirme. La même page s’affiche après un refus : c’est l’appareil qui l’indique.

Page de fin de TOSIAM : Done! Page de fin de TOSIAM : Done!
La page de fin, en anglais elle aussi.

Avec le lien direct, l’utilisateur passe directement de la connexion au consentement.

Étape 6 : le profil, puis la révocation#

Avec son access token, l’appareil lit le profil de l’utilisateur à userinfo, puis révoque le refresh token : l’appareil de l’exemple s’arrête là, et révoquer le refresh token met aussi fin à l’access token. La révocation est dans un finally : une fois les jetons obtenus, elle a lieu même si la lecture du profil échoue. Une révocation refusée n’est qu’un avertissement : la connexion a réussi.

javascript· appareil/appareil.js
try {
  const profile = await client.userinfo(tokens.accessToken);
  out(`Connecté : ${displayName(profile)}`);
  out(`Jeton d'accès valable ${minutes(tokens.expiresIn)} min (scope : ${tokens.scope ?? values.scope}).`);
} finally {
  try {
    await client.revoke(tokens.refreshToken ?? tokens.accessToken);
    out('Jetons révoqués.');
  } catch (e) {
    err(`Révocation impossible (${e.message}) : les jetons restent valables jusqu'à leur expiration.`);
  }
}

Étape 7 : lancer et tester#

bash
cd tosiam-device-flow
./up.sh
cd appareil
node appareil.js
texte
Appareil « appareil-tv » — TOSIAM http://localhost:8080/tosiam, royaume ref

Sur votre téléphone ou votre ordinateur, ouvrez : http://localhost:8080/tosiam/oauth2/ref/device/user
et saisissez le code : HC6FLMLQ
Lien direct : http://localhost:8080/tosiam/oauth2/ref/device/user?user_code=HC6FLMLQ

En attente de l'utilisateur (code valable 5 min)…

Ouvrez l’adresse, connectez-vous avec dduck / Donald-Duck-2026, saisissez le code et choisissez Autoriser. Quelques secondes plus tard :

texte
Connecté : Donald DUCK (dduck)
Jeton d'accès valable 60 min (scope : profile).
Jetons révoqués.

Avec Refuser, ou sans réponse pendant 5 minutes :

texte
Refusé : l'utilisateur n'a pas autorisé l'appareil (ou le code a déjà servi).
texte
Code expiré : l'utilisateur ne l'a pas validé à temps. Relancez l'appareil pour obtenir un nouveau code.
Faites défiler le tableau
Code de sortieSignification
0Appareil autorisé, profil lu, jetons révoqués
1Refusé, code expiré, ou réponse inattendue de TOSIAM
2Client ou royaume inconnu, ou client refusé par TOSIAM
3TOSIAM injoignable
130Attente interrompue (Ctrl-C)

En cas de problème#

Faites défiler le tableau
SymptômeCause probable
400 client_id, scope and response_type are required parametersresponse_type absent : TOSIAM l’exige, contrairement à la RFC 8628
invalid_grant Unknown Grant Type, urn:ietf:params:oauth:grant-type:device_codeGrant de la RFC 8628 : TOSIAM attend http://oauth.net/grant_type/device/1.0
400 code is a required parameterCode de l’appareil envoyé dans device_code : TOSIAM l’attend dans code
La page affiche unsupported_response_typeLe type de réponse n’est pas dans la liste du client, ou token seul avec le scope openid
La page affiche invalid_scopeUn scope demandé par l’appareil n’est pas dans les scopes du client
Réponse inattendue de TOSIAM : vérifiez TOSIAM_URL ou HTTP 404TOSIAM_URL désigne autre chose que TOSIAM (sans /tosiam, page d’un proxy…)
La page affiche « The code you entered cannot be found »Code mal recopié, expiré, ou déjà utilisé
slow_down à chaque appelL’appareil n’attend pas l’intervalle entre deux appels
invalid_clientClient inconnu dans ce royaume
Invalid realm, …Royaume inconnu (--royaume)
Le téléphone n’ouvre pas l’adresselocalhost désigne le téléphone lui-même : il faut une adresse de TOSIAM que le téléphone joint

En production#

  • HTTPS partout : le code de l’appareil et les jetons circulent dans les requêtes, et l’utilisateur se connecte sur la page de vérification.
  • Le device_code est un secret : jamais affiché ni journalisé. Seul le user_code est montré.
  • Un client public, sans secret : un appareil distribué ne garde pas de secret. Limitez ses scopes au strict nécessaire, et gardez le consentement : l’utilisateur doit voir quel appareil il autorise.
  • Respectez interval et ralentissez sur slow_down ; arrêtez d’interroger à l’expiration du code.
  • Les jetons sur l’appareil : rangez le refresh token dans le stockage protégé de l’appareil, et révoquez-le quand l’utilisateur dissocie l’appareil de son compte.
  • L’hameçonnage par code : quelqu’un peut envoyer à une victime un code qu’il a lui-même demandé. L’écran de consentement doit nommer clairement l’appareil ; des durées de code courtes limitent le risque.

Pour aller plus loin#

Mis à jour le