building mobile friendly apis in rails
TRANSCRIPT
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
user app server database
GET /users/1/profileHost: galaxies.comAuthorization: Bearer 09497d46978bf6f3226...
access_token: 09497d46978bf6f3226...6eabede90ce558ade5...
user_id: 17
admin: falsetrue
token_expires: 14669134901466913501
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
JSON Web Tokens
or JWTs.
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...
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.
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:
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
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...
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
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
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]
$ 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
$ 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
When a Rails Applicationis Behaving as an API
but Does Not Have VersioningImplemented... You Might
See a Lot of Monkeying Around.
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
# /users/current{ ”linked_device”:”none”, ”can_edit_health_metrics”:true, ”health_services_enabled”:false}
# User.swiftfunc canUseHealthKit() -> Bool { return linkedDevice == ”none” && canEditHealthMetrics && !healthServicesEnabled}
# SettingsViewController.swift
let user = Session.currentUser()if user.deviceHealthService == .Available { ...}