110
Building Mobile Friendly APIs in Rails

Building Mobile Friendly APIs in Rails

Embed Size (px)

Citation preview

Building Mobile Friendly APIs in Rails

Building Mobile Friendly APIs in Rails

Building Friendly APIs in Rails

HELLOMY NAME IS

Jim

Authentication

Support

Usability

Authentication

Support

Usability

Authentication

Support

Usability

TypicalAuthentication

user

user browser

user browser server

user browser server database

user browser server database

user browser server database

user_id: 1admin: falseprefers_mobile_site: true

user browser server

:cookie_store

:active_record_store

user browser server database

user browser server database

user_id: 1admin: falseprefers_desktop_site: false

session_id: 09497d46978bf6f32265fefb5cc52264

session_id: 09497d46978bf6f3226...6eabede90ce558ade5...

user_id: 17

admin: falsetrue

prefers_mobile:truetrue

Both Methods Rely onCookies to Maintainthe User’s Session

But...Cookies Only Work

in the Browser

What About a Mobile App?

user app server database

user app server database

user app server database

GET /users/1/profileHost: galaxies.comAuthorization: Bearer 09497d46978bf6f3226...

access_token: 09497d46978bf6f3226...6eabede90ce558ade5...

user_id: 17

admin: falsetrue

token_expires: 14669134901466913501

Wait... the :cookie_storedidn’t need to check withthe database everytime

We can avoid storing tokens in the database

using... TOKENS!

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

JSON Web Tokens

or JWTs.

header.payload.verify_signature

JSON Web Tokens

Encoded but not Encrypted

{ "alg": "HS256", "typ": "JWT"}

Header

Algorithm & Token Type

{ "iss": "your-service", "exp": 1300819380, "user_id": "1", "admin": true}

Payload

The Data

Payload Claims

http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html#RegisteredClaimNameRead More:

"iss": "your-service", "exp": 1300819380, "user_id": "1", "admin": true

RegisteredDefined by the spec.

PublicDefined byyour app

HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), SECRET_KEY_BASE)

Verify Signature

The Guarantee

JWTs are self containedso the server can confirmthe user is authenticated

by verifying the signature.

This is similar to how rails secures cookies...

How do I add JWTto my Project?

<<

Using Devise?Add a Warden Strategy

module Devise module Strategies class JsonWebToken < Base def valid? !request.headers['Authorization'].nil? end

def authenticate! if claims and user = User.find_by_id(claims.fetch('user_id')) success! user else fail! end end

private def claims auth_header = request.headers['Authorization'] and token = auth_header.split(' ').last and ::JsonWebToken.decode(token) rescue nil ...end

Determines whether ornot the strategy shouldbe ignored...

Provides the current_user

http://zacstewart.com/2015/05/14/using-json-web-tokens-to-authenticate-javascript-front-ends-on-rails.html

JWT + Devise TutorialsJWT via Warden Strategy by Zac Stewart:

I prefer this method over my own tutorial as I find the Warden strategy tobe more elegant than additional controller logic.

https://github.com/jimjeffers/rails-devise-cors-jwt-examplehttps://www.youtube.com/watch?v=_CAq-F2icp4

Rails + Devise + CORS + JWT Example & Screencast:

This is an example I put together for a client last year. The screencast got quitea bit of views and the example project may help you.

Using Something Else?

KnockSeamless JWT authentication for Rails API

class User < ActiveRecord::Base has_secure_passwordend

Works with any model that has an authenticate method

Like the one rails provides via has_secure_password...

https://github.com/nsarno/knock#knock

KnockSeamless JWT authentication for Rails API

class ApplicationController < ActionController::API include Knock::Authenticableend

class SecuredController < ApplicationController before_action :authenticate_user

def index # etc... endend

https://github.com/nsarno/knock#knock

JWT Works Within OAuthbut Doesn’t Replace it.

http://www.seedbox.com/en/blog/2015/06/05/oauth-2-vs-json-web-tokens-comment-securiser-un-api/

Check out this article explaining JWT vs. OAuth:

What about OAuth?

Doorkeeper.configure do access_token_generator "Doorkeeper::JWT"end

Doorkeeper

https://github.com/doorkeeper-gem/doorkeeper#custom-access-token-generator

https://github.com/chriswarren/doorkeeper-jwt

Doorkeeper Gem (custom access token generator):

Doorkeeper::JWT Gem:

What’s OAuth?I don’t have time to cover it in this talk

OAuth addresses securityconcerns such as revokingtokens or refreshing tokens

to allow your user to remain authenticated.

OAuth Resources

JWT + Refresh Tokens = OAuth2?https://stormpath.com/blog/jwt-authentication-angularjs

Try out Doorkeeperhttps://github.com/doorkeeper-gem/doorkeeper

Authentication(quick recap)

We can’t rely onrails’ magical cookies

Instead...implement a token based

authentication strategy

Consider... implementing OAuth via Doorkeeper

or Other Means

Authentication

Support

Usability

user web app serverV1V1

Updates to a monolithicrails web app / api...

user web app serverV2 V2

Are instant!

for the most part...

V2

Not with external clients! i.e. mobile apps!

V2

V1

You Lose Control of the Upgrade Process...

V3

V1 V3V3V2

V3

The Mobile Dev(s) Might Notbe on Their A-Game Slowing

Their Own Release Cycle.

Respective App Stores TakeTime to Review and Propogatethe Update to Your Mobile App

At least it’s down to about 1 day vs. a week...

Your Users Might Not Upgradefor a Variety of Reasons

2Ways I Deal With This...

1 Version the API

2 Track the Client Builds

Versioning is Trivialif You Start Early

Without VersioningCode Smell

is Highly Likely

...or You’ll ProbablyBreak the App

...at some point not immediately apparent...

Namespace Your Controllers

module V1class ArticlesController < ApplicationControllerdef index articles = Article.trendy respond_to do |format| format.json do render json: articlesendendendendend

Just Wrap’em in a Module!

Use a Route Constraint

This constraint parses the version from an HTTP Header.

class ApiConstraint attr_reader :version

def initialize(options) @version = options.fetch(:version) end

def matches?(request) request.headers.fetch(:accept).include?("version=#{version}") endend

Scope Your Routes

See the full tutorial on how to do this via Big Nerd Ranch:https://www.bignerdranch.com/blog/adding-versions-rails-api/

Rails.application.routes.draw do scope module: :v1, constraints: ApiConstraint.new(version: 1) do resources :articles, only: :index end

scope module: :v2, constraints: ApiConstraint.new(version: 2) do resources :articles, only: :index endend

Also Check Out Grape

http://www.ruby-grape.org

Mounts as an Engine

Rails.application.routes.draw do mount Twitter::API => '/'end

Supports Versioning

class Twitter::API < Grape::API mount Twitter::APIv1 mount Twitter::APIv2end

Has its Own DSL

module Twitter class Grape < Grape::API version 'v1', using: :header, vendor: 'twitter' format :json prefix :api

resource :statuses do desc 'Return a public timeline' get :public_timeline do Status.limit(20) endend

Separates API from Web App

app

api

modelsviewscontrollers

Grape solves a lot of problemsspecific to building an API

that Rails does not andperhaps is never meant to

[Rails is a web application framework]

2 Track Client Builds

Version the API

Multiple Iterations of the App Now Supported

V3

V1V3V3 V2

V3 V2 V1

What Happens When You Need/Wantto Stop Supporting a Version?

V3

V1V3V3 V2

V3 V2 V1

$ curl http://localhost:3000/api/builds/457 [{"support_level":”active”}]

Communicate to Your Users

Treat Builds/Platforms Like a Resource

$ curl http://localhost:3000/api/builds/457 [{"support_level":”supported”}]

Everything works as usual...

$ curl http://localhost:3000/api/builds/432 [{"support_level":”deprecated”}]

Prod User to Upgrade

oknot now

A new version isavailable. Youreally ought to download it!

$ curl http://localhost:3000/api/builds/289 [{"support_level":”unsupported”}]

Force User to Upgrade

Go to App Store

This Version isNot Supported

Several Releases of the App Canbe Using Any Version of the API

V3

V1V3V3 V2

V3 V2 V1

2.1.0

2.0.2

2.0.1

1.9.0

1.8.9

What if a Release Ships w/ aCatastrophic Bug?

V1V3V3 V22.1.0

2.0.2

2.0.1

1.9.0

1.8.9

$ curl http://localhost:3000/api/builds/457 [{"support_level":”unsupported”}]

Force User to Upgrade

Go to App Store

This Version isNot Supported

Supply the Build & Platform on All Requests

$ curl -H “X-API-Client: iOS_457” \ http://localhost:3000/api/articles

Patch the Emergencymodule Twitter class Grape < Grape::API ... helpers do def issue_119? headers[”X-API-Client”] == “iOS_457” end end resource :statuses do desc 'Return a public timeline' get :public_timeline do return fix_for_119 if issue_119? Status.limit(20) endend

Monkey Patching YourAPI Should Only be aTemporary Solution

FOR EMERGENCY USE ONLY

When a Rails Applicationis Behaving as an API

but Does Not Have VersioningImplemented... You Might

See a Lot of Monkeying Around.

Version the API

Track Client Builds

Authentication

Support

Usability

What’s Wrong with the Date?

{ event_name: “Beer ‘o Clock”, starts_at: “2015-07-04 5:00PM”}

See you at 5pm?Never EVER Forget about Timezones :)

PST

CST

EST

Rely on Standards

Time.now.utc.iso8601"2016-06-28T01:13:54Z"

Time.now.utc.to_i1467076518

Your Mobile Client Knows WhatTimezone Your User is in. Letit Convert UTC to Local Time.

Ruby is too Convenient!Which of the Following Should YouUse to Query Dates on the Server?

Date.today Time.currentsystem time application time

https://robots.thoughtbot.com/its-about-time-zones

Avoid Excessive BizLogic on the ClientWhen Possible

# /users/current{ ”linked_device”:”none”, ”can_edit_health_metrics”:true, ”health_services_enabled”:false}

# User.swiftfunc canUseHealthKit() -> Bool { return linkedDevice == ”none” && canEditHealthMetrics && !healthServicesEnabled}

# /users/current{ ... ”device_health_service”:”Available”, ...}

# SettingsViewController.swift

let user = Session.currentUser()if user.deviceHealthService == .Available { ...}

Saves You from DuplicateLogic in iOS and Android

Biz Logic is More Nimbleif You Can Avoid Hardcoding

it in the Client Libraries

Utilize Tokens Preferably Lightweight ones Like JWTs.

Have Your API and ClientShare their Versions.

Let the Client handle Formatting

Simplify Complex State on the Server

Keep things Friendly

THANKSANY QUESTIONS?