Popup OAuth ~ Guida Pratica

Popup OAuth ~ Guida Pratica

Scritto da Francesco Di Donato 27 aprile 2022 4 minuti di lettura

In un post precedente abbiamo implementato il flusso OAuth di GitHub da zero. Tuttavia, la reindirizzazione alla pagina di autenticazione avveniva tramite una reindirizzazione diretta, che è sicuramente un comportamento indesiderato, specialmente quando si tratta di una SPA.

Non sorprende che OAuth nel mondo reale utilizzi un approccio popup. Facile da risolvere, la reindirizzazione avverrà comunque, ma in un popup generato che, prima di chiudersi, passerà il access_token alla scheda principale.


Dove Ci Siamo Fermati

Questo post deve essere letto come un miglioramento del precedente, quindi ha un ritmo leggermente più veloce. In caso di dubbi, si prega di fare riferimento al codice di accompagnamento 🔗 o al post precedente 🔗.

<a id="oauth-github-popup">Authenticate via GitHub (popup)</a>
    <script>
      const CLIENT_ID = "Iv1.395930440f268143";

      const url = new URL("/login/oauth/authorize", "https://github.com");
      url.searchParams.set("client_id", CLIENT_ID);

      const cta = document.getElementById("oauth-github-popup");
      cta.setAttribute("href", url);
      cta.addEventListener("click", (event) => {
        event.preventDefault();

   // ...

Assembliamo il link di autorizzazione OAuth di GitHub e lo attacchiamo al tag <a> per ragioni di accessibilità. Tuttavia, ascoltiamo anche il clic su di esso, prevenendo la reindirizzazione diretta predefinita.

Utilizzando un popup

Successivamente, utilizzeremo l’API Web window.open 🔗 per generare il popup. Si aspetta come terzo parametro una stringa contenente width, height e altro.

Personalmente, preferisco l’esplicità di un oggetto che viene poi convertito nella stringa sopra menzionata.

Le proprietà top e left hanno il valore auto - questo non è nelle specifiche dell’API, infatti viene interpretato dal seguente frammento come un’istruzione per posizionarsi al centro dell’asse relativo. Fondamentalmente, se entrambi sono auto, il popup si aprirà sempre al centro, anche se cambi width o height.

// in the 'click' eventListener callback
const features = {
  popup: "yes",
  width: 600,
  height: 700,
  top: "auto",
  left: "auto",
  toolbar: "no",
  menubar: "no",
};

const strWindowsFeatures = Object.entries(features)
  .reduce((str, [key, value]) => {
    if (value == "auto") {
      if (key === "top") {
        const v = Math.round(
          window.innerHeight / 2 - features.height / 2
        );
        str += `top=${v},`;
      } else if (key === "left") {
        const v = Math.round(
          window.innerWidth / 2 - features.width / 2
        );
        str += `left=${v},`;
      }
      return str;
    }

    str += `${key}=${value},`;
    return str;
  }, "")
  .slice(0, -1); // remove last ',' (comma)

window.open(url, "_blank", strWindowsFeatures);

Quindi, l’utente clicca sul tuo elegante pulsante di GitHub e si autentica tramite il popup. Ma è importante istruire il server a rimandare a una pagina la cui funzione è:

  1. Assicurarsi che sia un popup
  2. Estrarre access_token dai parametri della query
  3. Inviarlo alla finestra principale (window.opener)
  4. Chiudersi 🔗

**Ricorda: callback OAuth Una volta che l’utente è autenticato, GitHub reindirizza all’URL di Callback specificato durante la creazione dell’OAuth App/GitHub App. Questo è trattato in modo più dettagliato nel post precedente.

server.get("/oauth/github/login/callback", async (request, reply) => {
  const { code } = request.query;

  const exchangeURL = new URL("login/oauth/access_token", "https://github.com");
  exchangeURL.searchParams.set("client_id", process.env.CLIENT_ID);
  exchangeURL.searchParams.set("client_secret", process.env.CLIENT_SECRET);
  exchangeURL.searchParams.set("code", code);

  const response = await axios.post(exchangeURL.toString(), null, {
    headers: {
      Accept: "application/json",
    },
  });

  const { access_token } = response.data;

  const redirectionURL = new URL("popup", "http://localhost:3000");
  redirectionURL.searchParams.set("access_token", access_token);

  reply.status(302).header("Location", redirectionURL).send();
});

server.get("/popup", (request, reply) => {
  return reply.sendFile("popup.html");
});

Il client viene reindirizzato a /popup e viene mostrato popup.html.

<script>
    if (window.opener == null) window.location = "/";

    const access_token = new URL(window.location).searchParams.get(
    "access_token"
    );

    window.opener.postMessage(access_token);
    
    window.close();
</script>

window.opener è null se la pagina non è stata aperta tramite window.open. In questo modo, se l’utente accede direttamente a /popup, viene reindirizzato a /.

Il calcolo è minimo, dovrebbe essere piuttosto veloce. Mostrare un caricatore, però, può solo giovarti.

Suggerimento Le SPA e le loro soluzioni di routing offrono alcuni metodi beforeEnter che possono essere associati a una rotta; beh, potresti controllare il valore di window.opener lì, per fornire un’esperienza ancora migliore.

Quasi fatto! Il popup sta restituendo l’access_token alla finestra principale, ma non sta ascoltando!

window.addEventListener("message", receiveMessage, false);

function receiveMessage(event) {
  if (event.origin !== window.location.origin) {
    console.warn(`Message received by ${event.origin}; IGNORED.`);
    return;
  }

  const access_token = event.data;
}

Come precauzione, ignora qualsiasi messaggio proveniente da un’altra origine. Si prega di considerare removeEventListener nel proprio codice. Salva l’access_token da qualche parte. Da questo punto, il flusso si riunisce al post precedente 🔗.

Non c’è nulla che ti impedisca di utilizzare questo schema anche per l’installazione dell’app GitHub e le modifiche ai permessi.