Skip to content

Implement Select Provider popup window capability #18

@dmitrizagidulin

Description

@dmitrizagidulin

If the app developer doesn't provide their own 'select provider' UI, it would be helpful if calling login() without passing in an IDP would open a Select Provider popup.

There's two workflow options for this popup window that we have to decide between -- either 1) it serves as a select provider UI only, and it's the main app window that redirects for the OAuth dance. Or 2) The redirect happens in the popup window, which is more convenient for the app developer, but involves more moving parts to implement.

Recommendation: We can start with implementing Option 1, since that's the easiest, and discuss the next steps.

Option 1 - Popup is Select Provider Only, Main App Window Redirects

pro: Implementing this requires less moving parts.

con: More inconvenient for the app developer; the user can potentially lose their place in the page after the resulting redirect (for some single-page js apps, like Tabulator).

Workflow:

  1. main window: login() is called with no IDP, popup window is opened, shows a list of IDPs (plus at least a textbox for user to enter their own domain or webid or preferred provider).
  2. popup: User clicks on a provider (or enters a url in the textbox), popup window sends a postMessage() event to the opener (main app window) with the url of the selected provider and closes itself.
  3. main window: Main window (the auth client) receives the providerSelected event, loads the RP client config for that IDP, constructs the /authorize URL, and redirects itself to login.
  4. main window: User logs in. When the IDP redirects the user to the post-login redirect_uri (usually the same URI as the main app was), the app logic calls authClient.currentSession() on page load, which results in the auth client parsing the callback uri and extracting the credentials from it, as usual.

Sample code snippets:

var selectProviderWindow
// providerSelectPopupSource is a string that contains the HTML + JS of the popup window

initEventListeners()

if (selectProviderWindow) {
  // Popup has already been opened
  selectProviderWindow.focus()
} else {
  // Open a new Provider Select popup window
  selectProviderWindow = window.open('',
    'selectProviderWindow',
    'menubar=no,resizable=yes,width=400,height=400'
  )

  selectProviderWindow.document.write(providerSelectPopupSource)
  selectProviderWindow.document.close()  // important
}

function initEventListeners() {
  window.addEventListener('message', function (event) {
    switch (event.data.event_type) {
      case 'providerSelected':
        dispatchProviderSelected(event.data.value)  // provider uri

        break
      default:
        console.error('unknown event type: ', event)

        break
  })
}

In the popup window:

// opener is a browser global
function selectProvider (providerUri) {
  console.log('Provider selected: ', providerUri)
  
  var message = {
    event_type: 'providerSelected',
    value: providerUri
  }

  opener.postMessage(message, opener.window.location.origin)
  // close yourself
  window.close()
}

Option 2 - Popup is Select Provider + Redirect to Login, Main Window Not Redirected

pro: More convenient for the app developer (user's place / workflow in the main app page is not interrupted).

con: More moving parts to implement (would require alteration to solid-server & config).

Workflow:

  1. main window: Decide on the callback redirect_uri during RP dynamic registration (see the callback uri discussion below).
  2. main window: login() is called with no IDP, popup window is opened, shows a list of IDPs (plus at least a textbox for user to enter their own domain or webid or preferred provider).
  3. popup: User clicks on a provider (or enters a url in the textbox), popup window sends a postMessage() event to the opener (main app window) with the url of the selected provider, and stays open.
  4. main window: Main window (the auth client) receives the providerSelected event, loads the RP client config for that IDP, constructs the /authorize URL, and redirects the popup window to login.
  5. popup: User logs in. When the IDP redirects the popup to the post-login redirect_uri, some logic in the redirect_uri page must do a postMessage to the opener (main app page) with its own uri (containing hash fragments with credentials), and closes itself. (See the discussion below).
  6. main window: Receives the authCallback event from the popup window, with the callback uri as the event payload. Extracts the credentials from that uri, as usual (instead of currentSession deriving them from the main page's own current uri).

The tricky bit is step 5. What should be the redirect_uri that the RP client pre-registers, and asks to be redirected to during the authentication workflow? There's basically 3 options:

  1. The rp client uses its own URI as a redirect_uri (like it currently does). In order for this to work, though, the main app code needs to have a snippet like:

    // in the 'onload' window event
    if (opener) {
      opener.postMessage({ event_type: 'authCallback', value: window.location.href }, opener.window.location.origin)
      window.close()  // close yourself
    }

    as soon as the page loads (so that the full app doesn't load in the popup window).

  2. The app developer includes a callback.html (see below) as part of their web application, and tells the RP to register it as the redirect_uri.

  3. We include a callback.html in the /common/ folder of all node-solid-server installations, and advertise the callback_uri endpoint either via a Link rel header, or via a service config document. The RP discovers this uri, and includes it in the redirect_uris list during dynamic registration.

Sample callback.html:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta http-equiv="Content-type" content="text/html;charset=utf-8">
  <script type="text/javascript">
    window.addEventListener('load', function () {
      var callbackUrl = window.location.href
      var opener = window.opener
      var pageOrigin = opener.location.origin

      var message = {
        event_type: 'authCallback',
        value: callbackUrl
      }

      opener.postMessage(message, pageOrigin)
      window.close()
    })
  </script>
</head>
<body>
<h1>Login successful</h1>

<p>Please close this window.</p>
</body>
</html>

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions