Typing a username and password on the Apple TV is cumbersome and annoying. For the NSScreencast TV app, I decided to implement a code-based authentication where you can easily log in on another device, type in the code, and have the device be logged in automatically. In this episode we'll go over how to implement this, starting with the server. This episode is done entirely in Ruby using the Sinatra web application framework, but the technique is applicable to any server side technology (including Swift!).
Episode Links Source Code Sinatra - A lightweight web application framework. Redis - A powerful key value store that is super easy to use. We'll use this to store temporary data that gets cleaned up automatically. Guide to installing Ruby (and Rails) on Mac OS X - If you want to follow along but don't have a ruby environment setup yet, this guide can be helpful. This guide is particular to Rails, but you can simply start with the installing Ruby directions to get rbenv, which is what I use. Paw - A great HTTP workbench tool. Setting up Redis If you don't have redis installed, it's easily installed via homebrew: $ brew install redis Setting up Sinatra We'll start by defining our Gemfile: source "https://rubygems.org" gem 'sinatra' gem 'json' gem 'redis' gem 'shotgun' We're using Sinatra here, which is a rack-compatible web framework. Adding shotgun will allow us to make changes to the app and not have to restart the process every single time, which is a must in development. To install these, run: $ bundle install To start up the application, we'll use a rackup file: # config.ru require 'rubygems' require 'sinatra' require './app' run Sinatra::Application Our app.rb looks like this: require 'sinatra' require 'json' require 'redis' get '/' do "hello" end Now run the server: $ shotgun If you visit http://localhost:9393 in your browser, you should get the "hello" message. You're ready to start! Getting the Auth Code We'll support retrieving a code with a simple endpoint: post '/easy_auth' do require_client_id token = SecureRandom.hex(24) client_id = params['client_id'] key = "auth_req:#{token}" code = (rand() * 90000).to_i + 10000 # TODO: avoid collisions with codes data = { client_id: client_id, token: token, code: code, status: 'pending' } redis.setex(key, TIMEOUT, data.to_json) redis.setex("auth_lookup:#{code}", TIMEOUT, key) data.to_json end def redis Redis.current end def require_client_id halt 400, "client_id is required" if params['client_id'].nil? end Use the provided Paw file to test this out and verify you get a code. Activate Form Next we'll add an activation form. This would typically be behind your login, so you'd know who the user is at this point. We'll skip that step for simplicity and just fake an auth token. get '/activate' do if params['error'] @error = params['error'] end erb :activate end Inside of views/activate.erb: <h1>activate your device</h1> <div style="color: red"><%= @error %></div> <form method="post" action="activate"> <input type="text" name="code"> <input type="submit"> </form> Now we need to handle the POST: post '/activate' do code = params['code'] if key = redis.get("auth_lookup:#{code}") data = redis.get(key) json = JSON.parse(data) auth_token = SecureRandom.hex(24) json['auth_token'] = auth_token json['status'] = 'authenticated' redis.setex(key, TIMEOUT, json.to_json) redirect '/success' else redirect '/activate?error=invalid-code' end end get '/success' do "Success! Your device will log in automatically." end Adding a status endpoint The device will be polling this status endpoint in order to see when the user has authenticated. get '/easy_auth/:token' do |token| key = "auth_req:#{token}" if data = redis.get(key) json = JSON.parse(data) return json.to_json else halt 404, 'token not valid or expired' end end And that's it! Test it out with Paw and verify you can see an auth_token in this response. Next time we'll talk about how to better secure this value so we don't transmit it in plain-text to any client that knows about the token.