
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
- 564