Clever Idea – Invitations with Angular and OAuth

I’ve decided to start a bit of a series of blog posts called “Clever Idea”. I’ll use the space to talk about something I’m working on either in my hobby project (an app for picking Football games with your friends) or at work that I think is clever. The intent will partly be to share a technique or technology that I think some people reading might be interested in. I’ll also be hoping that occasionally someone points out to me that I’m not as clever as I thought and there’s actually a better way to accomplish what I’m trying to accomplish.

The Problem

As some of you know (and I’ll be posting about more), I have run a little app for picking football games the last 4 or 5 years. It allows a “league” of people to keep score on who does the best picking games against the spread. This year I am making an improvement to allow users who are already in the league to form new leagues. A user is asked to specify the email addresses of everyone they’d like to participate in the league. If you specify someone who is already in the league, then they are added to your list of players. The first problem comes in when you want to invite someone who isn’t currently enrolled in the system.

I use Google OAuth for authentication. So I will need to “pair” the identity that an existing user has specified as wanting to be in their league with a logged in Google ID. Note that I feel this is necessary because not everyone uses the email address associated with their Google ID as their primary email address, so I would not want to make it impossible to join unless the person inviting you happened to guess the right email. One of my friends gave me the great suggestion of providing users a link. If I send an email to the new player and say they’ve been invited to play in a league, I can include a link with a query string parameter that includes their “user id” (a random GUID created by my system). This is where things get more difficult. Unfortunately (in this case), I’m using Angular for my frontend. Angular creates a downloadable browser app that players in the league use to interact with the system and that keeps itself refreshed through backend API calls. I’m a huge fan of Angular, BUT it makes my “pairing link” impossible.

Angular is a fully downloaded app that doesn’t actually go fetch other pages when the URL changes (these are handled client-side through the Angular “router”). Consequently, I can’t send someone directly to the “pairing” section of my app because the URL doesn’t “exist” on the web. In their deployment instructions, Angular themselves recommend forcing all incoming traffic to a specific URL via your app server. Seemingly this leaves me with no choice but to use an unfriendly system where I force users to land at my strange website for the first time, trust it enough to “login” even though they aren’t yet users, and paste their “pairing ID” (the GUID that represents them in my system) in to the system so it recognizes who they are.

The Clever Idea

Once you hear the idea, it seems obvious. However, I had fully coded the crappy user experience mentioned above before I came up with it… Just build a simple webpage outside of your Angular deployment artifacts, but located alongside your app. This means that Google will recognize the host as white listed when someone tries to login, your users will trust it as your site, AND you can send people to a specific page with query string parameters. Below is the simple HTML/javascript page that I setup for the purpose (you can find it here on git):

<html lang="en">
<head>
  <meta name="google-signin-scope" content="profile email">
  <meta name="google-signin-client_id" content="<id>.apps.googleusercontent.com">
  <script src="https://apis.google.com/js/platform.js" async defer></script>
</head>
<body>
<h1>Welcome to the New Player Page for LTHOI</h1>
To pair, please follow these steps:
<ol>
  <li>If you don't want to play Leave the House Out of It, do NOT fill in this form.  Your invitation will expire and the person who invited you will be alerted.  You will be removed from our database entirely.</li>
  <li>Signin to your Google Account.
    <div class="g-signin2" data-onsuccess="onSignIn" data-theme="dark"></div></li>
  <li>Fill in your first name and last initial.<br>
    <input type="text" id="fn" value="Your First Name"><br>
    <input type="text" id="li" value="Your last initial"><br>
    <button align="center" onclick="onSubmit()">Accept Invitation to Play Leave The House Out of It</button></li>
</ol>
<p id="error"></p>
<script>
  var access_token="Unregistered";
  function onSignIn(googleUser)
  {
    // The ID token you need to pass to your backend:
    access_token = googleUser.getAuthResponse().access_token;
  }
  function onSubmit()
  {
    // Get the user_id from the QSP to pass
    var urlParams = new URLSearchParams(window.location.search);
    var uid = urlParams.get('uid');
    var request = new XMLHttpRequest();
    request.open('POST', ('<BackEnd>/bff/pair?uid=' + uid), true);
    request.setRequestHeader('googleToken', access_token);
    try {
      request.send('{ "first_name" : "' + document.getElementById("fn").value + '", "last_initial" : "' + document.getElementById("li").value + '" }');
      if (request.status != 200)
      {
        document.getElementById("error").innerText = 'That did not work.  Are you sure you were invited?  logged in?';
      }
      else
      {
        window.location.replace("<dev location of LTHOI so they can login>");
      }
    }
    catch (err)
    {
      document.getElementById("error").innerText = 'That did not work.  Are you sure you were invited?  logged in?';
    }
  }
</script>
</body>
</html>

Comment
Name
Email