OAuth popup ~ Practical Guide
Tags: javascript, tutorial, webdev
Published: 2022-04-27 08:11:13

In a [previous post](https://dev.to/didof/github-app-and-oauth-practical-kick-starter-4df4) we have implemented the GitHub OAuth flow from scratch. However, the redirection to the authentication page was via hard redirection, which is definitely not wanted behavior especially when you're dealing with a SPA.

Not surprisingly _OAuth in the wild_ use a **popup** approach. Easy to solve, the redirection will still happen but in a spawned popup that, before dissolving will pass the `access_token` back to main tab.

***

> This post is to be read as an enhancement of the previous one, so it has a slightly higher pacing. If in doubt, please refer to the [accompanying code](https://github.com/didof/github-app-standalone/tree/popup) or the [previous post](https://dev.to/didof/github-app-and-oauth-practical-kick-starter-4df4).

```html
<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();

   // ...
```

we assemble the GitHub OAuth authorization link and attacch it to the `<a>` tag for _accessibility reason_. However, we also listen for click on it, **preventing** the default **hard redirection**.

Next we are going to use the Web API [window.open](https://developer.mozilla.org/en-US/docs/Web/API/Window/open) to spawn the popup.
It expect as third parameter a string containing `width`, `height` and more.

Personally, I prefer the explicity of an object that is then converted to the above mentioned string.

> `top` and `left` properties have the `auto` value - that's not in the specifications of the API, in fact it is interpreted by the following snippet as an instruction to place in the center of the relative axis. Basically if both are `auto` the popup will always spawn in the center, even if you change `width` or `height`.

```js
// 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);
```

Thus, the user clicks your fancy GitHub button and authenticate via the popup. But it is important to instruct the server to send back to some page whose function is:
1. Ensure it is a popup
2. Extrapolate `access_token` from query params
3. Dispatch it to parent window (`window.opener`)
4. [Close itself](https://en.wikipedia.org/wiki/Seppuku)

{% details Remember: OAuth callback %} 
Once the user gets authenticated, GitHub redirects to the **Callback URL** specified at OAuth App/GitHub App creation. This is addressed in more detail in the [previous post](#github-app-practical-kick-starter).
{% enddetails %}

```js
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");
});
```

The client gets redirected to `/popup` and `popup.html` is shown.

```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` is `null` if the page has not been opened via the `window.open`. This way, if the user directly routes to `/popup`, gets redirected to `/`.

The computation is minimal, it should be pretty fast. Showing a spinner though can only do you good.

{% details Tip %} 
SPA and their router solutions offers some `beforeEnter` method that can be associated with a route; well, you could check the `window.opener` value in there, in order to provide an even better experience.
{% enddetails %}

Almost done! The popup is dashing the `access_token` back to the parent but it is not listening!

```js
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;
}
```

As a precaution ignore any message coming from another origin. Please, consider `removeEventListener` in your code.
Save the `access_token` somewhere. From this point the flow rejoins the [previous post](https://dev.to/didof/github-app-and-oauth-practical-kick-starter-4df4).

There's nothing stopping you from using this pattern for GitHub App installation and permissions changes as well.

***

### Related Posts <a name="related-posts"></a>

- [GitHub App - Practical Kick-Starter (Standalone flow)](https://dev.to/didof/github-app-and-oauth-practical-kick-starter-4df4)
- [GitHub App - Practical Kick-Starter (Disjointed flow)](https://dev.to/didof/github-app-and-oauth-disjointed-flow-5a2o)

- [cover image](https://octodex.github.com/jetpacktocat/)

### Contacts

- [Twitter](https://twitter.com/did0f)
- [LinkedIn](https://www.linkedin.com/in/didof/)
- [GitHub](https://github.com/didof)