Skip to content

Instantly share code, notes, and snippets.

@evadne
Created July 23, 2015 07:51
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save evadne/0ad4d309a87972058950 to your computer and use it in GitHub Desktop.
Save evadne/0ad4d309a87972058950 to your computer and use it in GitHub Desktop.
Ephemeral Secure Token with JWT
class EphemeralToken
attr_reader :origin, :targets, :expires_at, :payload
class TokenInvalid < StandardError; end
class TokenExpired < TokenInvalid; end
Algorithm = 'HS512'
Secret = ENV['SECRET_EPHEMERAL_TOKEN_KEY']
ObjectToNotation = -> (target) { [target.class.model_name.name, target.id] }
NotationToObject = -> ((model_name, model_id)) { model_name.constantize.find_by_id(model_id) }
class << self
def parse (token)
payload, _ = decode_jwt(token)
data, exp = payload.values_at('data', 'exp')
new({
origin: data['origin'].try(&NotationToObject),
targets: data['targets'].map(&NotationToObject).compact,
expires_at: exp.try { |x| Time.at(x) }
})
end
private
def decode_jwt (token)
JWT.decode(token, Secret)
rescue JWT::ExpiredSignature
raise TokenExpired.new 'the token has expired'
rescue JWT::DecodeError
raise TokenInvalid.new 'the token is not valid'
end
end
def initialize (origin: nil, targets: [], expires_at: nil)
@origin = origin
@targets = Array(targets)
@expires_at = expires_at
end
def payload
@payload ||= build_payload
end
def to_jwt
JWT.encode payload, Secret, Algorithm
end
def has_target? (target)
targets.any? { |x|
(x.id == target.id) && (x.class.model_name.name == target.class.model_name.name)
}
end
private
def build_payload
{
data: {
origin: origin.try(&ObjectToNotation),
targets: targets.map(&ObjectToNotation)
}
}.tap { |x|
if expires_at.present?
x[:exp] = expires_at.to_time.to_i
end
}
end
end
module Concerns::HasPreviewToken
extend ActiveSupport::Concern
def authorize_preview! (target)
unless preview_token.has_target?(target)
deny_preview
end
rescue EphemeralToken::TokenInvalid
deny_preview
end
def deny_preview
raise CanCan::AccessDenied.new('you are not authorized to view this page.')
end
def preview_token
@preview_token ||= EphemeralToken.parse(params[:preview_token])
end
end
@joshgoebel
Copy link

Neat. Does JWT use any randomness to generate different hashes for the same input, or does a fixed input and expiration (and secret) always result in exactly the same token?

@joshgoebel
Copy link

Do you generally prefer raising and catch errors rather than something like EphemeralToken.parse(token_string).valid??

@evadne
Copy link
Author

evadne commented Aug 20, 2015

@yyyc514:

I have a Rails Controller Concern as follows, using CanCan, which I mix into controllers that utilize it:

module Concerns::HasPreviewToken
  extend ActiveSupport::Concern

  def authorize_preview! (target)
    unless preview_token.has_target?(target)
      deny_preview
    end
  rescue EphemeralToken::TokenInvalid
    deny_preview
  end

  def deny_preview
    raise CanCan::AccessDenied.new('you are not authorized to view this page.')
  end

  def preview_token
    @preview_token ||= EphemeralToken.parse(params[:preview_token])
  end
end

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment