Skip to content

Instantly share code, notes, and snippets.

@jimsynz
Last active December 27, 2018 07:29
Show Gist options
  • Save jimsynz/49b22ff8554f4b1bb6cc to your computer and use it in GitHub Desktop.
Save jimsynz/49b22ff8554f4b1bb6cc to your computer and use it in GitHub Desktop.
API Session Token Example
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_factory
raise NoFactoryError unless current_user.factory
current_user.factory
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
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
class ApiSessionTokenSerializer < ApplicationSerializer
attributes :token, :ttl
has_one :user
end
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
class Api::V1::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
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment