Created
March 19, 2014 01:42
-
-
Save jimsynz/9633932 to your computer and use it in GitHub Desktop.
Ember API Token
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# app/controllers/api_controller.rb | |
class ApiController < ApplicationController | |
skip_before_action :verify_authenticity_token | |
respond_to :json | |
rescue_from UserAuthenticationService::NotAuthorized, with: :not_authorized | |
rescue_from ActiveRecord::RecordNotFound, with: :not_found | |
before_filter :api_session_token_authenticate! | |
private | |
def signed_in? | |
!!current_api_session_token.user | |
end | |
def current_user | |
current_api_session_token.user | |
end | |
def api_session_token_authenticate! | |
return not_authorized unless authorization_header && current_api_session_token.valid? | |
end | |
def ensure_signed_in! | |
return not_authorized unless current_user | |
end | |
def current_api_session_token(token=authorization_header) | |
@current_api_session_token ||= ApiSessionToken.new(token) | |
end | |
def authorization_header | |
request.headers['HTTP_AUTHORIZATION'] | |
end | |
def not_authorized message = "Not Authorized" | |
error message, 401 | |
end | |
def not_found message = "Not Found" | |
error message, 404 | |
end | |
def not_acceptable message = "Not acceptable" | |
error message, 406 | |
end | |
def bad_request message = "Bad request" | |
error message, 400 | |
end | |
def error message, status | |
render json: { error: message }, status: status | |
end | |
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# app/assets/javascripts/models/api_session_token.coffee | |
App.ApiSessionToken = Em.Object.extend | |
token: null | |
ttl: 0 | |
userId: null | |
status: 0 | |
user: (-> | |
App.User.find(@get('userId')) if @get('hasUser') | |
).property('userId') | |
isAlive: (-> @get('ttl') > 0).property('ttl') | |
hasToken: (-> | |
token = @get('token') | |
if token && (typeof(token) == 'string') && (token.length > 0) | |
true | |
else | |
false | |
).property('token') | |
hasUser: Em.computed.notEmpty('userId') | |
isValid: (-> | |
@get('hasToken') && @get('isAlive') && !@get('isInError') | |
).property('hasToken', 'isAlive') | |
isInError: (-> | |
@get('status') > 399 && @get('status') != 401 | |
).property('status') | |
needsRefresh: (-> | |
ttl = @get('ttl') | |
if @get('isInError') | |
ttl < 1 | |
else | |
ttl < 11 | |
).property('ttl', 'isInError') | |
init: -> | |
@scheduleTtlDecrement() | |
decrementTtl: -> | |
@decrementProperty('ttl') if @get('isAlive') | |
@scheduleTtlDecrement() | |
scheduleTtlDecrement: -> | |
Em.run.later(this, @decrementTtl, 1000) | |
autoRefresh: (-> | |
if @get('needsRefresh') && !@get('isRefreshing') | |
@refresh() | |
).observes('needsRefresh', 'isRefreshing') | |
storeTokenInLocalStorage: (-> localStorage['token'] = @get('token')).observes('token') | |
authenticate: (username,password)-> | |
App.ApiSessionToken.acquire(@, {username: username, password: password}) | |
authenticateWithApiSecret: (username, apiSecret)-> | |
apiKey = (new jsSHA("#{username}:#{apiSecret}:#{@get('token')}", 'TEXT')).getHash('SHA-256', 'HEX') | |
App.ApiSessionToken.acquire(@, {username: username, api_key: apiKey}) | |
refresh: -> | |
App.ApiSessionToken.acquire(@) | |
abandon: -> | |
localStorage.removeItem('token') | |
localStorage.removeItem('api_username') | |
localStorage.removeItem('api_secret') | |
token = @ | |
Em.run -> | |
new Em.RSVP.Promise (resolve,reject)-> | |
$.ajax | |
dataType: 'json' | |
data: { token: token.get('token') } | |
url: "/sessions" | |
type: 'DELETE' | |
success: -> | |
resolve(token.refresh()) | |
error: (xhr,status,error)-> | |
reject(error) | |
App.ApiSessionToken.reopenClass | |
acquire: (token, credentials={})-> | |
Em.run -> | |
token ||= App.ApiSessionToken.create(token: localStorage['token']) | |
token.set('isRefreshing', true) | |
credentials.token = if token.get('hasToken') | |
token.get('token') | |
else | |
localStorage['token'] | |
new Em.RSVP.Promise (resolve,reject)-> | |
$.ajax | |
dataType: 'json' | |
data: credentials | |
url: "/sessions" | |
type: 'POST' | |
success: (data,status,xhr)-> | |
Em.run -> | |
token.setProperties | |
token: data.api_session_token.token | |
ttl: data.api_session_token.ttl | |
userId: data.api_session_token.user_id | |
status: xhr.status | |
message: xhr.statusText | |
isRefreshing: false | |
resolve(token) | |
error: (xhr,status,error)-> | |
Em.run -> | |
token.setProperties | |
status: xhr.status | |
message: xhr.statusText | |
ttl: 10 | |
isRefreshing: false | |
reject(token) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# app/models/api_session_token.rb | |
require 'json_serializing_model' | |
class ApiSessionToken | |
extend ActiveModel::Naming | |
include ActiveModel::Serialization | |
include JsonSerializingModel | |
TTL = 20.minutes | |
UnknownToken = Class.new(RuntimeError) | |
def initialize(existing_token=nil, redis=ApiSessionToken.redis_connection) | |
@redis = redis | |
@token = existing_token if valid_existing_token? existing_token | |
unless expired? | |
self.last_seen = Time.now | |
self.user = user if user | |
end | |
end | |
def token | |
@token ||= MicroToken.generate 128 | |
end | |
def ttl | |
return 0 if @deleted | |
return TTL unless last_seen | |
elapsed = Time.now - last_seen | |
remaining = (TTL - elapsed).floor | |
remaining > 0 ? remaining : 0 | |
end | |
def last_seen | |
@last_seen ||= retrieve_last_seen | |
end | |
def last_seen=(as_at) | |
set_with_expire(last_seen_key, as_at.iso8601) | |
@last_seen = as_at | |
end | |
def user | |
return if expired? | |
@user ||= retrieve_user | |
end | |
def user=(user) | |
set_with_expire(user_id_key, user.id) | |
@user = user | |
end | |
def expired? | |
ttl < 1 | |
end | |
def valid? | |
!expired? | |
end | |
def deleted? | |
@deleted | |
end | |
def delete! | |
@redis.del(last_seen_key, user_id_key) | |
@deleted = true | |
end | |
private | |
def valid_existing_token? t | |
t && retrieve_last_seen(t) | |
end | |
def set_with_expire(key,val) | |
@redis[key] = val | |
@redis.expire(key, TTL) | |
end | |
def retrieve_last_seen(t=token) | |
ls = @redis[last_seen_key(t)] | |
ls && Time.parse(ls) | |
end | |
def retrieve_user | |
user_id = @redis[user_id_key] | |
User.find(user_id) if user_id | |
end | |
def last_seen_key(t=token) | |
"session_token/#{t}/last_seen" | |
end | |
def user_id_key | |
"session_token/#{token}/user_id" | |
end | |
def self.redis_connection | |
@redis ||= begin | |
opts = {} | |
opts[:driver] = :hiredis | |
Redis.new opts | |
end | |
end | |
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# app/serializers/api_session_token_serializer.rb | |
class ApiSessionTokenSerializer < ApplicationSerializer | |
attributes :token, :ttl | |
has_one :user | |
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# app/assets/javascripts/routes/application_route.coffee | |
App.ApplicationRoute = Em.Route.extend | |
model: -> | |
if localStorage.api_username? && localStorage.api_secret? | |
App.ApiSessionToken.acquire().then (token)-> | |
if token.get('hasUser') | |
token | |
else | |
token.authenticateWithApiSecret(localStorage.api_username, localStorage.api_secret) | |
else | |
App.ApiSessionToken.acquire() | |
afterModel: (model)-> | |
App.set('sessionToken', model) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# lib/json_serializing_model.rb | |
module JsonSerializingModel | |
def active_model_serializer | |
"#{self.class.to_s}Serializer".constantize | |
end | |
def serialize_to_json(serializer=active_model_serializer) | |
serializer.new(self).as_json.to_json | |
end | |
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# app/controllers/sessions_controller.rb | |
class Api::SessionsController < ApiController | |
skip_before_filter :api_session_token_authenticate!, only: [:create] | |
def create | |
token = current_api_session_token(params[:token] || authorization_header) | |
if params[:username] | |
@user = User.confirmed.find_by_username(params[:username]) | |
token.user = @user if provided_valid_password? || provided_valid_api_key? | |
end | |
respond_with token | |
end | |
def show | |
respond_with current_api_session_token | |
end | |
def destroy | |
current_api_session_token.delete! | |
render nothing: true, status: 204 | |
end | |
private | |
def provided_valid_password? | |
params[:password] && UserAuthenticationService.authenticate_with_password!(@user, params[:password]) | |
end | |
def provided_valid_api_key? | |
params[:api_key] && UserAuthenticationService.authenticate_with_api_key!(@user, params[:api_key], current_api_session_token.token) | |
end | |
def api_session_token_url(token) | |
api_v1_sessions_path(token) | |
end | |
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# app/services/user_authentication_service.rb | |
module UserAuthenticationService | |
NotAuthorized = Class.new(Exception) | |
module_function | |
def authenticate_with_password(user, attempt) | |
user && BCrypt::Password.new(user.password) == attempt | |
end | |
def authenticate_with_password!(*args) | |
authenticate_with_password(*args) or raise NotAuthorized | |
end | |
def authenticate_with_api_key(user, key, current_token) | |
user && key && current_token && OpenSSL::Digest::SHA256.new("#{user.username}:#{user.api_secret}:#{current_token}") == key | |
end | |
def authenticate_with_api_key!(*args) | |
authenticate_with_api_key(*args) or raise NotAuthorized | |
end | |
end |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# app/assets/javascripts/controllers/users_sign_in_controller.coffee | |
# Most of this could be moved to a route. | |
App.UsersSignInController = Em.Controller.extend | |
needs: ['application'] | |
sessionTokenBinding: 'controllers.application.model' | |
init: -> | |
@_super() | |
@send('reset') | |
username: null | |
password: null | |
errors: null | |
rememberMe: false | |
hasUsername: Em.computed.bool('username') | |
hasPassword: Em.computed.bool('password') | |
signInButtonEnabled: Em.computed.and('hasUsername', 'hasPassword') | |
signInButtonDisabled: Em.computed.not('signInButtonEnabled') | |
actions: | |
reset: -> | |
@setProperties(username: null, password: null, errors: null, rememberMe: false) | |
attemptSignIn: -> | |
onSuccess = (token)=> | |
token.get('user').then (user)=> | |
user.get('session').refresh(user).then => | |
@send('authenticationDidSucceed', user) | |
onFailure = (token)=> | |
@send('authenticationFailed') | |
username = @get('username') | |
password = @get('password') | |
@get('sessionToken').authenticate(username, password).then(onSuccess,onFailure) | |
authenticationDidSucceed: (user)-> | |
@send('storeApiSecret', user) if @get('rememberMe') | |
@send('reset') | |
@transitionToRoute('user.index', user) | |
authenticationFailed: -> | |
@set('errors', Em.Object.create(password: ['Authentication failed'])) | |
storeApiSecret: (user)-> | |
if user.get('hasApiSecret') | |
localStorage['api_username'] = user.get('username') | |
localStorage['api_secret'] = user.get('apiSecret') |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment