Last active
December 27, 2018 07:29
-
-
Save jimsynz/49b22ff8554f4b1bb6cc to your computer and use it in GitHub Desktop.
API Session Token Example
This file contains 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
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 |
This file contains 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
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 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
class ApiSessionTokenSerializer < ApplicationSerializer | |
attributes :token, :ttl | |
has_one :user | |
end |
This file contains 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
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 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
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 |
This file contains 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
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