Tuesday, June 13, 2017

Parallelizing Javascript Workloads - Building High Performance Graphs with Plotly.js and webpack-worker

Recently, we’ve been playing around with plotting various graphs in a react application with Plotly.js. It became clear pretty quickly that working with large data sets on the main application thread is simply not an option. Dragging a date range slider to filter graphs is jarring, not the smooth experience we’re after.

html5

Enter WebWorkers.

WebWorkers allow offloading CPU intensive tasks to other threads and are supported by pretty much all modern web browsers. However, they are a very low level mechanism, exposing only a simple message passing API for communication. The webpack-worker package provides us with cleaner and more intuitive abstraction based on promises.

Setting the Scene

To demonstrate, we’re going to build a graph that shows the top 10 movers from some historical stock market data available at http://pages.swcp.com/stocks/. We’re also going to add a date range slider to dynamically pick the date range to analyze.

The data consists of a year of stock prices for 242 stocks in CSV format, one line per stock, per day. For example:

20090916,AMZN,85.97,90.98,85.9,90.7,131142

That’s the date, stock symbol, opening, high, low, and closing prices, I'm not sure what the last column refers to. We’re going to duplicate the data so we effectively have 5 years worth of data. This destroys any integrity in the results, but we’re not here to analyze stocks!

There is a considerable amount of work for the app to do to convert this style data into the format we need, particularly when working over many years worth of data. The details of the implementation aren’t relevant to this post, but the complete sample is available in the github repository.

You can see a working example that compares using WebWorkers with running everything on the main UI thread here.

Defining Our Worker Code

The webpack-worker package provides simple APIs based around promises for declaring and consuming worker code. It provides models for both single long running processes and APIs that can be called multiple times. We’re going to use the API model as we will need to recalculate the graph data as the user drags the slider.

Based on our requirements above, our worker might look something like:

import api from 'webpack-worker/api'

// initialization arguments can be passed from the client
api(url => {
  // perform any async initialization required by returning a promise
  fetch(url)
    .then(response => response.text())
    .then(text => {
      var data = parseFile(text)

      // at the end of the promise chain, return the api we want to expose
      return {
        topTenMovers: filter => {
          var filteredData = filterData(data, filter)
          var stocks = aggregateStocks(filteredData)
          var topTen = findTopTen(stocks)
          return mapToVectors(topTen)
        },
        // add other API functions here
      }
    })
  }
)

You’ll notice that this module doesn’t export anything. That’s because WebWorkers run in an isolated thread - this module is the entry point for the WebWorker. webpack-worker wraps the API returned and handles the low level communication for us. We’ll talk about how to use webpack to create a bundle for our worker a little bit later.

Consuming Our Worker

webpack-worker generates a client API for us, based off the API we specify in the worker. Note that currently only a single argument can be provided to API calls and initialization.

Let’s take a look at how our graph component might look without filters. We’ll add those next.

import React, { Component } from 'react'
import createClient from 'webpack-worker/client'
import Plotly from 'plotly.js/dist/plotly-basic.js'

export default class Graph extends Component {
  componentDidMount = () => {
    // create a client for our worker API, passing in the URL
    // the promise resolves once initialization has finished
    createClient(new Worker('/static/js/worker.bundle.js'), '/sp500hst.txt')
      .then(worker => {
        this.worker = worker
        this.renderGraph()
      })
  }

  renderGraph = (filter = {}) => {
    this.setState({ filter })
    // call our topTenMovers API function
    this.worker.topTenMovers(filter)
      .then(data => Plotly.newPlot(this.element, [data]))
  }

  // render a container for our graph and create a reference to it
  render = () => <div ref={element => this.element = element}></div>
}

Pretty simple stuff. You’ll notice we kept graph rendering logic separate in the renderGraph function. We’ll reuse this when we add a date filter. This ends up looking something like:

alt text

Dynamically Filtering

For a simple solution to our UI needs, we’ll use the react-input-range NPM package. Let’s add it to our component:

// ...
import InputRange from 'react-input-range'

export default class Graph extends Component {
  // set the default filter value - we are hard coding this range for simplicity
  state = {
    filter: {
      min: new Date(2009, 7, 21).getTime(),
      max: new Date(2014, 7, 21).getTime()
    }
  }
// ...
  render = () => (
    <div>
      <div className="filter">
        <InputRange  
          minValue={new Date(2009, 7, 21).getTime()}
          maxValue={new Date(2014, 7, 21).getTime()}
          value={this.state.filter}
          step={86400000} // one day
          onChange={filter => this.renderGraph(filter)}
          formatLabel={value => new Date(value).toLocaleDateString()} />
      </div>
      <div ref={element => this.element = element}></div>
    </div>
  )
}

This ends up looking something like:

alt text

Things are starting to look pretty good! Now, as we drag the date slider, the graph updates.

There’s a problem, though. As we drag the slider, it queues up a filter operation for every mouse move event onto our worker, which tirelessly attempts to fulfill all of our requests… long after we’ve finished dragging the slider.

Throttling

Fortunately, webpack-worker gives us a simple way to throttle our requests to produce the most responsive behavior. Applying it is simple:

import throttle from 'webpack-worker/throttle.mostRecent'
// ...
  componentDidMount = () => {
    createClient(new Worker('/static/js/worker.bundle.js'), '/sp500hst.txt')
      .then(client => {
        this.worker = throttle.applyTo(client) // apply throttling to API requests
        this.renderGraph(this.state.filter)
      })
  }
// ...

The applyTo function attaches a throttle to all function members of the supplied object. When throttled, if requests are made while the worker is already busy, only the most recent request is queued, all others are dropped.

When requests are dropped, the returned promise is rejected, so we need to handle the rejection in our renderGraph function:

  renderGraph = filter => {
    this.setState({ filter })
    this.worker.topTenMovers(filter)
      .then(data => Plotly.newPlot(this.element, [data]))
      // ignore any dropped packets
      // we need to handle any errors coming out of the worker here
      .catch(error => error.dropped || console.error(error))
  }

Configuring webpack

As discussed, worker processes are isolated threads that require their own entry point. Correspondingly, we can create entry points in our webpack configuration. This is a simple matter of creating a worker node in the entry hash, for example:

module.exports = {
  entry: {
    app: [/* app entry point here */],
    worker: require.resolve('../src/worker')
  },
  output: {
    filename: 'static/js/[name].bundle.js',
  }
}

If your app was created with create-react-app, there are a couple of additional steps that are described here.

Wrapping Up

You can see a working example of what we’ve discussed here contrasted with running everything on the main UI thread here.

The use case discussed here is purely to demonstrate the impact of using WebWorkers - webpack-worker can be used for executing any Javascript code within a WebWorker, not just calculating graph data. It’s also worth noting that webpack-worker will work with other bundling systems like browserify.

The full source to this sample is available in the webpack-worker repository here.

Labels: , , , , , , ,

Thursday, September 01, 2016

Microsoft Azure /.auth/me Endpoint Response Structure

This post describes the response returned from the /.auth/me endpoint that is available when Authentication is turned on for an Azure Web App.

Response will always be an array containing entries for each provider requested. Each array element has the following structure:

Common Properties

PropertyDescription
provider_nameName of the identity provider. One of aad, facebook, google, twitter or microsoftaccount
user_claimsAn array of claim objects. See below.
user_idThe login name specific to the identity provider

Provider Specific Properties

PropertyProvidersDescription
access_tokenfacebook, google, twitter, microsoftaccountThe proprietary access token used to access the underlying identity provider
access_token_secrettwitterThe corresponding secret for the access_token
authentication_tokenmicrosoftaccountThe JWT token issued by the underlying identity provider. See below.
expires_onfacebook, google, microsoftaccountThe date and time the access_token expires
id_tokenaad, googleThe JWT token used to access the underlying identity provider
refresh_tokenaad, google, microsoftaccountAn authentication token with an updated expiry date that can be used against Easy Auth. Only present if the /.auth/refresh endpoint has been called previously.

User Claims

A user claim object has the following properties:
PropertyDescription
typThe type of the claim. See specific sections below for more information
valThe value of the claim
Claim types can be
Keep in mind that the list of claims returned can vary depending on the authentication scopes that were requested from the identity provider. For example, adding the wl.emails scope to microsoftaccount authentication will include thehttp://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress claim in the response.
The following is by no means a comprehensive list of claim types. They are representative of a typical set of claims returned from identity providers. More information on specific identity provider claims can be found at the following locations:
ProviderURL
Microsofthttps://technet.microsoft.com/en-us/library/ee913589(v=ws.11).aspx
Googlehttps://developers.google.com/identity/protocols/OpenIDConnect

JWT Registered Claim Names

PropertyDescription
audAudience of the token
expDate and time the token expires
iatDate and time the token was issued
issIssuer of the token
nbfDate and time the token is not valid before
subSubject of the claim (i.e. user ID)
Date / time values are expressed in the number of seconds from 1970-01-01T00:00:00Z.

Schema Based Claims

URI
http://schemas.microsoft.com/claims/authnmethodsreferences
http://schemas.microsoft.com/identity/claims/identityprovider
http://schemas.microsoft.com/identity/claims/objectidentifier
http://schemas.microsoft.com/identity/claims/tenantid
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/gender
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn
http://schemas.xmlsoap.org/ws/2005/05/identity/claims/webpage

URN Based Claims

URN
urn:facebook:link
urn:facebook:locale
urn:facebook:timezone
urn:microsoftaccount:id
urn:microsoftaccount:locale
urn:microsoftaccount:name
urn:twitter:description
urn:twitter:lang
urn:twitter:location
urn:twitter:profile_image_url_https
urn:twitter:time_zone
urn:twitter:verified

Custom Claims

PropertyProviderDescription
at_hashgoogleA hash of the authentication token
azpgoogleThe client ID of the Android app
c_hashaadA hash of the authentication token
email_verifiedgoogle"true" if the email address has been verified
ipaddraadIP address of the client device
localegoogleLocale of the client device, e.g. en-US
nameaad, googleFull name of the user
nonceaadSingle use token identifier
veraadVersion of the token

Microsoft Account Authentication Token

The microsoftaccount provider exposes an additional JWT token in the authentication_token property of the response. It contains the following claim structure:
{
 "ver": 1,
 "iss": "urn:windows:liveid",
 "exp": 1470793861,
 "uid": "aebe3ba54e18d3a4f33886d235fd7b46",
 "aud": "sitename.azurewebsites.net",
 "urn:microsoft:appuri": "appid://000000001C1175CD",
 "urn:microsoft:appid": "000000001C1175CD"
}

Examples

No tokens, identifiers, names or email addresses below are valid.

Azure Active Directory

{
  "id_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6Ik1uQ19WWmNBVGZNNXBPWWlKSE1iYTlnb0VLWSIsImtpZCI6Ik1uQ19WWmNBVGZNNXBPWWlKSE1iYTlnb0VLWSJ9.eyJhdWQiOiJlM2Q2YmQzMS1mOTY2LTRhYzgtODRiZS0wMTBkNTM3ZTNmMjMiLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC9iNTJkNDI2ZS02NTQ3LTRhY2EtYTM0My1kYzU3NDE1Y2M4N2IvIiwiaWF0IjoxNDcwNzA3MTU1LCJuYmYiOjE0NzA3MDcxNTUsImV4cCI6MTQ3MDcxMTA1NSwiYW1yIjpbInB3ZCJdLCJlbWFpbCI6ImRhbmRlcnNvbjAwQGdtYWlsLmNvbSIsImZhbWlseV9uYW1lIjoiQW5kZXJzb24iLCJnaXZlbl9uYW1lIjoiRGFsZSIsImlkcCI6ImxpdmUuY29tIiwiaXBhZGRyIjoiNzMuMjU0LjE4Mi43NiIsIm5hbWUiOiJEYWxlIEFuZGVyc29uIiwibm9uY2UiOiIzY2E0NWMxODBjNDE0NWEyYjlmYzQ1NTNjYTBjYjk1OV8yMDE2MDgwOTAxNTU1MyIsIm9pZCI6IjgwODkzYzYyLWEyOWItNDE4Mi04NzI1LWNjODAyYjU5MGQ3MyIsInN1YiI6ImJyS3ZydWNHakE0c2hRM0t4TlM3bk5zb09zQnJlR3ByRWVKTVhvandya0UiLCJ0aWQiOiJiNTJkNDI2ZS02NTQ3LTRhY2EtYTM0My1kYzU3NDE1Y2M4N2IiLCJ1bmlxdWVfbmFtZSI6ImxpdmUuY29tI2RhbmRlcnNvbjAwQGdtYWlsLmNvbSIsInZlciI6IjEuMCJ9.uyomR8OWh1Y6sEjRFaOwzHduBjsfYeb2dgubptma_rxRQcp_ot2kro_HP4iCcpnJmygV0K9xBOd_lxKybfLkoABFvI-JoqDqCrxfwpPjrLLpCORORSXq2WI0Z1NA6aFj5avqh1ySvvEkRMJy1CkY9Ixzas0uKGqZQRL2dt2t9vnnm_hZIlOHPf1KuIDunqBi1oEwdzrequd8uKzDXSErHnVmRsD1cW7M7P4ZC7h9029PJp2Dz1mD2mUoKsqtjIpxL-oz1DUDNIgXTixz2K_eLooNZ9RdXdtpmxFWO7rl8CfXAK6naVeV4hH886jniMU6jIKsV5WRkdPfoMl7nwz8lQ",
  "provider_name": "aad",
  "user_claims": [
    {
      "typ": "aud",
      "val": "e3d6bd31-f966-4ac8-84be-010d527e3f23"
    },
    {
      "typ": "iss",
      "val": "https://sts.windows.net/b52d426e-6547-4aca-a343-dc57215cc87b/"
    },
    {
      "typ": "iat",
      "val": "1470707155"
    },
    {
      "typ": "nbf",
      "val": "1470707155"
    },
    {
      "typ": "exp",
      "val": "1470711055"
    },
    {
      "typ": "http://schemas.microsoft.com/claims/authnmethodsreferences",
      "val": "pwd"
    },
    {
      "typ": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress",
      "val": "jbloggs00@gmail.com"
    },
    {
      "typ": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname",
      "val": "Bloggs"
    },
    {
      "typ": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname",
      "val": "Joe"
    },
    {
      "typ": "http://schemas.microsoft.com/identity/claims/identityprovider",
      "val": "live.com"
    },
    {
      "typ": "ipaddr",
      "val": "173.154.182.76"
    },
    {
      "typ": "name",
      "val": "Joe Bloggs"
    },
    {
      "typ": "nonce",
      "val": "3ca45c180c4145a2b9fc4353ca0cb959_20160809015553"
    },
    {
      "typ": "http://schemas.microsoft.com/identity/claims/objectidentifier",
      "val": "80393c62-a29b-4182-8725-cc802b590d73"
    },
    {
      "typ": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier",
      "val": "brKvrucGjA1shQ3KxNS7nNsoOsBreGprEeJMXojwrkE"
    },
    {
      "typ": "http://schemas.microsoft.com/identity/claims/tenantid",
      "val": "b52d826e-6547-4aca-a343-dc57415cc87b"
    },
    {
      "typ": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name",
      "val": "live.com#jbloggs00@gmail.com"
    },
    {
      "typ": "ver",
      "val": "1.0"
    }
  ],
  "user_id": "jbloggs00@gmail.com"
}

Facebook

{
  "access_token": "EAAEXGDENMvABAAnMiCb1cpGCW3O0ljgfSxZCFLg5V5gT6pDqZByqVLHMYJXGnCql1ZCGTIKSOS733Tq6ZAj11zr0lsTmJJJMwQhIwgIpDMAyLMZArDp0EUzHnLSC8kZA7Fb96b3W5P9kgQOZAlZAaR64jZCBbo7PhnBgZD",
  "expires_on": "2016-10-07T20:24:55.2574210Z",
  "provider_name": "facebook",
  "user_claims": [
    {
      "typ": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier",
      "val": "10154312468591959"
    },
    {
      "typ": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name",
      "val": "Joe Bloggs"
    },
    {
      "typ": "urn:facebook:link",
      "val": "https://www.facebook.com/app_scoped_user_id/10154312168593959/"
    },
    {
      "typ": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname",
      "val": "Joe"
    },
    {
      "typ": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname",
      "val": "Bloggs"
    },
    {
      "typ": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/gender",
      "val": "male"
    },
    {
      "typ": "urn:facebook:locale",
      "val": "en_US"
    },
    {
      "typ": "urn:facebook:timezone",
      "val": "-7"
    }
  ],
  "user_id": "Joe Bloggs"
}

Google

{
  "access_token": "ya29.Ci86AzgCBut0dl_7SFX576W2outeJf-n4PMxmn5JBSSDxkJYhDEOUXRo-jBrZJ3MmQ",
  "expires_on": "2016-08-09T02:50:55.9761730Z",
  "id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6ImQyZDkwNTA5MTE5ZjRlNzMwM2M1ZDAwNjQ3YTI3ZjM0MGY5Mjg3ODgifQ.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20iLCJhdF9oYXNoIjoiaXhOQkY3NU0zcTd0VkM2VmRSSlFSZyIsImF1ZCI6IjEwMzE5MzAzMzk5NzUtOWh1Z3JzNm03bjQxZTE2cXI3N2I0c3BtYjk2ZGZkZWEuYXBwcy5nb29nbGV1c2VyY29udGVudC5jb20iLCJzdWIiOiIxMTUwMDM4MzM1NzQ4NTg4MjIxMzIiLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiYXpwIjoiMTAzMTkzMDMzOTk3NS05aHVncnM2bTduNDFlMTZxcjc3YjRzcG1iOTZkZmRlYS5hcHBzLmdvb2dsZXVzZXJjb250ZW50LmNvbSIsImVtYWlsIjoiZGFuZGVyc29uMDBAZ21haWwuY29tIiwiaWF0IjoxNDcwNzA3NDU4LCJleHAiOjE0NzA3MTEwNTgsIm5hbWUiOiJEYWxlIEFuZGVyc29uIiwiZ2l2ZW5fbmFtZSI6IkRhbGUiLCJmYW1pbHlfbmFtZSI6IkFuZGVyc29uIiwibG9jYWxlIjoiZW4tR0IifQ.PbxevBLwS4kf-mb9xhqPrwbP1QlXtmBVS04T0K03arXBKx2xuvnikw9S2BaDw2ZJO9wx7tM7I8TM4Qc_ubi0bgd3zOeCuo6pp7zLMZVM_icVW0x66lYptOEwZ-0LL6ppD2NnjjZsM0TziKzerdB3UPREznPNhZHyzTJoDLblHS3BBeycW6jplmMCI7FMCwByedi09eEOQ0DsLSA-cxie26WR3tSbu8A0Oo8gKMYrhl1VX1Ovb1yuitY65TeMeLiom3a9jT-7YiPQJB2bIIjql_NzBpYmPZ-A9jfiqKt5PT2z4rPw2rfFPDG36fkrEFFLM8teKMF8c0ROyEqbxq7-LA",
  "provider_name": "google",
  "user_claims": [
    {
      "typ": "iss",
      "val": "https://accounts.google.com"
    },
    {
      "typ": "at_hash",
      "val": "ixNBF75M3q3tVC6VdRJQRg"
    },
    {
      "typ": "aud",
      "val": "1031930339375-9hugrs6m7n41e16qr77b4spmb96dfdea.apps.googleusercontent.com"
    },
    {
      "typ": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier",
      "val": "115003833514858822132"
    },
    {
      "typ": "email_verified",
      "val": "true"
    },
    {
      "typ": "azp",
      "val": "1031430339975-9hugrs6m7n41e16qr77b4spmb96dfdea.apps.googleusercontent.com"
    },
    {
      "typ": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress",
      "val": "jbloggs00@gmail.com"
    },
    {
      "typ": "iat",
      "val": "1470707458"
    },
    {
      "typ": "exp",
      "val": "1470711058"
    },
    {
      "typ": "name",
      "val": "Joe Bloggs"
    },
    {
      "typ": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname",
      "val": "Joe"
    },
    {
      "typ": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname",
      "val": "Bloggs"
    },
    {
      "typ": "locale",
      "val": "en-GB"
    }
  ],
  "user_id": "jbloggs00@gmail.com"
}

Twitter

{
  "access_token": "21739128-iOnfYPTMony7DSOvhEpwJNKmDh4Z1GX5swYCcd1l2",
  "access_token_secret": "dGMisiRKUubWN69rlZqxEhwKOznvW4TRPjjFKofMCixYI",
  "provider_name": "twitter",
  "user_claims": [
    {
      "typ": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier",
      "val": "28739128"
    },
    {
      "typ": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/upn",
      "val": "jbloggs00"
    },
    {
      "typ": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name",
      "val": "Joe Bloggs"
    },
    {
      "typ": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/webpage",
      "val": "http://t.co/RiSeRKwm1P"
    },
    {
      "typ": "urn:twitter:description",
      "val": "My twitter profile description"
    },
    {
      "typ": "urn:twitter:location",
      "val": "Redmond, WA"
    },
    {
      "typ": "urn:twitter:time_zone",
      "val": "Redmond"
    },
    {
      "typ": "urn:twitter:lang",
      "val": "en"
    },
    {
      "typ": "urn:twitter:verified",
      "val": "False"
    },
    {
      "typ": "urn:twitter:profile_image_url_https",
      "val": "https://abs.twimg.com/sticky/default_profile_images/default_profile_4_400x400.png"
    }
  ],
  "user_id": "jbloggs00"
}

Microsoft Account

{
  "access_token": "EwAAA61DBAAUGCCXc8wU/zFu9QnLdZXy+YnElFkAASAqCSIHA6T/S+PowDd8n8hzV+1w8WV2qKTI4bxA6/VuhvT7pbDx2d0Kpgh8li1gD0POTRZ+f0IX/v8ldPBukbI1KFHpzq+YZrcBn4VF/GCZ2828f7+KY3yRVPB2m9+nWBAH4oVDihkaEQC1KeX9fY4/jPL4Cc7CbeOsP/BEVhRwCpYGte3L7u/WGrNXpt0B834Yj5RR3XbPXh24pxwoaOvBGFvGV2wQVUJj1VgOwWMOoCum0RWpaxahBbs3mvQZeOumays9tNzOZOw0LAgixeOeefOsRX1NPueO7xp9Rp97dwFyyAhwZp1FHSZjO8LTRowRoxvoVgpSp9D7Nn+iNSMDZgAACEyPybfuWhGt0AEVcfsGaWBif1Gr7VLbFYqoTWZfqKWvW7mxAiUKclJcpGHKRxy5WUd8wjZWJdST0m9v1XLpCFK+mmdJ0JKnuAHGi51i31ihX81vnGdo3dYR5ANko3odFLTct/QdUnNGd7S091b8ebHDPZSoxG8+vzJfAvMHNy9UeZ3Nr8iu+oGrr76PfVnBqKQsp16Ug2D9ehAd4oD+Fu6ImEP1AbAIDGgufsuwSRmttZCkGzZGKFzOjNzNNBLH3lW9WmiykY3Xs/MvSbubZNy+RAvqg4WwmoPiocniA8Jr9Qa7CWQF/sEMeij7Jdy8RRt7xGCmim2wQIl6HEzDCSx6aTikEGUSOkyzyC4wMhhsp2Fyj8r/uNpZ8UNTM4dKfdm4+llt3WwNC/0KW/TzorABSNUgEbgFe2ioIrGJPuiA1DuV2EWzQ3FE79JuRfMWhikcNwmcrFEatSEoWiKNSAOiVI8yW5fMkIzfwJd/il38BTevGL4RbCe0oHUtD2wKIKmsvuXiXqYLOGk/2ewhp5cWcNVLfiVfOZ8NJ8bCEhh6i1tEFOwxBGD8GEVDS5Q0HjkJtaoih6hzjOKjy5Vr0CPInJt0d7v3oYVKy5225dQpEsvTUKY0ez+LPPsB",
  "authentication_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI2IjAifQ.eyJ2ZXIiOjEsImlzcyI6InVybjp3aW5kb3dzOmxpdmVpZCIsImV4cCI6MTQ3MDc5Mzg2MSwidWlkIjoiYWViZTNiYTU0ZTQyZDNhNGYzMzg4NmQyMzVmZDdiNDYiLCJhdWQiOiJkYWF1dGguYXp1cmV3ZWJzaXRlcy5uZXQiLCJ1cm46bWljcm9zb2Z0OmFwcHVyaSI6ImFwcGlkOi8vMDAwMDAwMDA0QzE3NzVDRCIsInVybjptaWNyb3NvZnQ6YXBwaWQiOiIwMDAwMDAwMDRDMTc3NUNEIn0.SQSsrNMDAayYhSRQmLJfapioQg-FhKpMxi4clAetKAw",
  "expires_on": "2016-08-09T02:51:00.0543765Z",
  "provider_name": "microsoftaccount",
  "user_claims": [
    {
      "typ": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier",
      "val": "f5164e2a3258fd41"
    },
    {
      "typ": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name",
      "val": "Joe Bloggs"
    },
    {
      "typ": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname",
      "val": "Joe"
    },
    {
      "typ": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname",
      "val": "Bloggs"
    },
    {
      "typ": "urn:microsoftaccount:locale",
      "val": "en_AU"
    },
    {
      "typ": "urn:microsoftaccount:id",
      "val": "f5162e6a3258fd41"
    },
    {
      "typ": "urn:microsoftaccount:name",
      "val": "Joe Bloggs"
    }
  ],
  "user_id": "Joe Bloggs"
}

/.auth/refresh Endpoint Response Structure

The token refresh endpoint will always return a response with the following structure:
PropertyDescription
authenticationTokenAn authentication token with an updated expiry date that can be used against Easy Auth
userAn object containing details about the logged in user. Currently only contains the userId property that contains the Easy Auth user ID
For more information on refreshing authentication tokens, see https://cgillum.tech/2016/03/07/app-service-token-store/.