
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 domainAXDMRpc 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?
Squerb Microservices
By Lucjan Suski
Squerb Microservices
- 553
 
   
   
  