🏠 back to Observable

Best practice for OAuth popup prompts?


#1

Given a dependency on a service that requires client-side authentication, such as an external API, I need the viewer to click a button and open a pop-up window confirming their Google/Facebook/GitHub account. Any examples or suggestions for how to do this well? With the criteria:

  1. Minimal boilerplate for new notebooks. If I need to write a small my-service-observable-auth NPM dependency to hide the boilerplate, that’s fine.
  2. Clear UX for non-technical readers of the notebook. Would ideally show the “Authenticate” button at the top of the notebook if they’re logged out, and next to nothing otherwise.

I’m not trying to password-protect the notebook, it just requires an authenticated API as a datasource. Currently my code looks something like this:

authStatus = new Promise((resolve, reject) => {

  // Service client ID.
  const CLIENT_ID = '<CLIENT_ID>';

  service.auth(CLIENT_ID, function onSuccess () {

    // User is already logged in.
    resolve('authenticated from session');

  }, function onError () {

    // User not logged in. Try opening a popup.
    service.openAuthPopup(
      () => resolve('authenticated with popup'),
      () => reject('failed to authenticate with popup')
    );

  });
})

But this doesn’t let me conditionally show a button (if logged out) to avoid having my popups blocked by the browser — I think that requires spreading my authentication boilerplate over multiple cells? Is there a more idiomatic way to write this?


#2

Sure, here’s an example notebook that simulates an OAuth-style login flow:

It uses views to define the sign in button, such that the value of the view is the user’s login.


#3

@donmccurdy The example does not work with uBlock or Adblock enabled.


#4

Thanks @mike! That approach looks workable to me. :slight_smile:

@Cameri my code above was just an example; it doesn’t run. Or do you mean that OAuth popups sometimes do not work with ad-blockers? That may be unavoidable in this situation.


#5

One thing that may improve the example is a way to let the rest of the notebook hang until authentication has happened, rather than trying to execute cells and reporting authentication errors. I worked around this by creating a wrapper for my service similar to your example, and having that wrapper yield access to the service client only after authentication succeeds. The result is concise:

Message if logged in, sign in button if not.

viewof auth = serviceWrapper.auth()

Some actual notebook content:

result = {
  const value =  service.getResourceThatFailsWithoutAuth();
  // ...
  return result;
}

Wrapper around the normal service client, providing helpers for auth:

serviceWrapper = {
  const MyServiceWrapper = await require('my-service-wrapper');
  const CLIENT_ID = '<client-id>';
  return new MyServiceWrapper({html, md}, CLIENT_ID);
}

Helper method that yields my service client only after authentication has succeeded:

service = serviceWrapper.getService()

#6

That’s the way my authentication simulator works: if a cell references user and the reader is not signed in, the cell won’t be evaluated until the user signs in. This works by setting the initial value of the view to undefined, and then setting it to the user’s login and dispatching an input event after the user successfully signs in.

If you take a look at the implementation of Generators.input, notice that it doesn’t call change (the generator doesn’t initially yield a value) if the input’s value is initially undefined.


#7

Oops, your example is very clear about that now that I’ve re-read it. Thank you for the explanation, this is working well. :slight_smile:


#8

Have been battling with the same challenge while integrating Google Sign In to access a private API. Got it working nicely and decided to make a demo out of it:

Enjoy! :wink:

-Albert