Episode #220

Easy Auth - Part 1

20 minutes
Published on May 12, 2016

This video is only available to subscribers. Get access to this video and 586 others.

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.

This episode uses Tvos 9.0.