Lucjan Suski

Untitled Talk

aka What's going on at Squerb

Lucjan Suski

#1 - Authentication Service

Responsibilities:

  • sign in users using email/password
  • create users based on provided data
  • sign in/create users using OAuth2 protocol
  • manage authentication tokens across domains

Stack:

  • Rails 4.1
  • Omniauth
  • Some Javascript

Lucjan Suski

Manage authentication tokens across domains

  • cookies storage
    • problems on iOS/Safari
    • unable to share across diferrent domains
  • localStorage / sessionStorage
    • unable to share across diferrent domains, but...

Lucjan Suski

Solution #1

  • keep the token on single domain
  • access it through postMessage

Problem

Each time user loads the page, we have to wait for the iframe to access the token

Lucjan Suski

Solution #2

Push token after login using iframes / postMessage instead of pulling it.

Problem

Does not work on iOS due to very strict privacy policy - it doesn't allow to modify localStorage in iframe on another domain

Lucjan Suski

Solution #3

 

Push token after login using redirection chain

Problem

Outage of one authenticatable service means breaking the redirection chain

Lucjan Suski

Oauth authentication flow

  • user clicks link, pointing to http://auth.squerb.com/auth/facebook
  • start polling of localStorage for auth token change
  • omniauth does the stuff and retrieves user data
  • basing on uid provided, decide if we have this user or not, to handle registration / sign in
  • retrieve/generate authentication token for this user
  • POST redirect to http://squerb.com/auth_proxy with token and list of domains which are using the token (widgets.squerb.com)
  • set the token in localStorage, redirect to next domain from list / close the window if it's empty

Lucjan Suski

Email / password authentication flow

 

It's very similar to previous one, the only difference is that we are POSTing data with email/password at the beginning, instead of just calling omniauth endpoint.

Lucjan Suski

Does it work?

Lucjan Suski

Gimme code already!

Lucjan Suski

  def callback
    user = Authenticator.new(auth_hash).authenticate
    @token = fetch_auth_token(user)
    @domains = ENV['SYNCED_DOMAINS'].split(' ')
  end

<form action="<%= @domains[0] %>/auth_proxy" method="POST">
  <input type="hidden" value='<%= @domains[1..-1].to_json.html_safe %>' name="domains" />
  <input type="hidden" value="<%= @token %>" name="token" />
</form>
<script>
  document.getElementsByTagName('form')[0].submit()
</script>

http://auth.squerb.com/auth/facebook/callback

Lucjan Suski

@AuthProxy =
  call: (options) ->
    @updateToken(options.token)
    if options.domains.length
      @redirectTo(options.domains, options.token)
    else
      window.close()

  updateToken: (token) ->
    localStorage.setItem('squerb_auth_token', token)

  redirectTo: (domains, token) ->
    form = createForm(action: "#{domains[0]}/auth_proxy", token: token, domains: domains[1..-1])
    form.submit()

Squerb Auth Proxy gem

Lucjan Suski

React login component

 

  pollToken: =>
    @lastToken = @get() || Date.now()
    setInterval =>
      if @get() != @lastToken
        @lastToken = @get()
        @trigger 'update'
        @onChange()
    , 1000
@PopupLogin = React.createClass

  render: ->
    <Popup className="popup-login" title="Sign in to Squerb" ref="popup">
      { @props.children }
      <div>
        <SquerbAuth.Login token=CurrentUser.token onSuccess={@_closePopup} />
      </div>
    </Popup>

  _closePopup: ->
    @refs.popup._hide()

Lucjan Suski

#2 - Widgets

(and how to deal with postMessage and CORS)

Lucjan Suski

Take any client's page, put short JavaScript snippet and then... magic happens.

<script src="http://squerb-widgets-staging.herokuapp.com/voting.js"></script>
<script>
  $(document).ready(function(){
    widget = SquerbWidgets.voting({ 
        draggable: '.gallery-icon', 
        category: 'product', 
        title: 'What iPhone 6 will you buy, or not?', poll_id: 1 })
  })
</script>

Lucjan Suski

How does it look like?

Lucjan Suski

So, what exactly happens?

 

  • download some CSS
  • insert iframe, which contains widget
  • make certain elements of the page draggable
  • send crossdomain message with position of element
  • react properly in the iframe containing widget
  • do some cross-origin ajax request in the meantime

Lucjan Suski

Abstraction over postMessage

Motivations:

  • pure postMessage is awkward to use
  • easyXDM is too complex

Lucjan Suski

class @XDMStream
  constructor: (options = {}) ->
    @stream = _.extend({}, Events)
    @socket = new XDMSocket
      onMessage: @onMessage
      remote: options.remote
      channel: options.channel
      whitelist: options.whitelist

  on: (event, callback) ->
    @stream.on(event, callback)

  off: (event, callback) ->
    @stream.off(event)

  trigger: (name, data) ->
    @socket.send({ name, data })

  onMessage: (message) =>
    @stream.trigger(message.name, message.data)

XDMStream

Lucjan Suski

# domainA/some_endpoint

testStream = new XDMStream(channel: 'test', remote: 'domainB/some_endpoint')
testStream.on('another-message', -> console.log('message received on domainA'))

# at some point
testStream.trigger('test-message')

# domainB/some_endpoint

testStream = new XDMStream(channel: 'test', whitelist: 'domainA')
testStream.on('test-message', -> console.log('message received on domainB!'))

# at some point
testStream.trigger('another-message')

XDMStream usage

Lucjan Suski

class @XDMRpc
  constructor: (options = {}) ->
    @stream = new XDMStream
      remote: options.remote
      channel: options.channel
      whitelist: options.whitelist

  call: (name, attrs...) ->
    d = D()
    @stream.on "#{name}_response_success", d.resolve
    @stream.on "#{name}_response_failure", d.reject
    @stream.trigger("#{name}_request", attrs)
    d.promise

  register: (name, fn) ->
    @stream.on "#{name}_request", (attrs) =>
      D.promisify(fn(attrs...)).then (value) =>
        @stream.trigger "#{name}_response_success", value
      , (value) =>
        @stream.trigger "#{name}_response_failure", value

XDMRpc

Lucjan Suski

# domainA/some_endpoint

rpc = new XDMRpc(channel: 'test', whitelist: ['domainB'])
rpc.register('someMethod', -> console.log('some method called'))

# domainB/some_endpoint

rpc = new XDMRpc(channel: 'test', remote: 'domainA/some_endpoint')
rpc.call('someMethod') # will print 'some method called' on domainA

XDMRpc usage

Lucjan Suski

What about this CORS?!

  • forget about setting CORS HTTP headers, messing with withCredentials and stuff like that
  • it's not reliable, especially on iOS / Safari

Lucjan Suski

Use XDomain

  • great library by jpillora: https://github.com/jpillora/xdomain
  • proxies every XMLHttpRequest to iframe
  • easy to set up / use, especially with my gem for Rails: https://github.com/methyl/xdomain-rails

Lucjan Suski

Slave domain configuration

  • slave domain is domain you want to access with CORS 
# routes.rb
mount XdomainRails::Engine, at: '/xdomain'
# config/initializers/xdomain-rails.rb

XdomainRails.configure do |config|
  config.master_domains = http:/master.example.com
end

Lucjan Suski

Master domain configuration

  • master domain is a domain which is making requests
# application.html.erb, before any javascript file

<%= xdomain_slaves %>
# config/initializers/xdomain-rails.rb

XdomainRails.configure do |config|
  config.slave_domains = http://slave.example.com
end
  • from now on, every ajax request will just work
$.get('http://slave.example.com/secret.json')

Lucjan Suski

That's it, thank you!

Questions?

Made with Slides.com