Skip to content

Instantly share code, notes, and snippets.

@bradgessler
Last active August 31, 2023 17:12
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bradgessler/ac4a5809749a81e0570e4b0e8500a035 to your computer and use it in GitHub Desktop.
Save bradgessler/ac4a5809749a81e0570e4b0e8500a035 to your computer and use it in GitHub Desktop.
NoPassword II
require "openssl"
require "securerandom"
require "bundler/inline"
gemfile do
gem "anybase"
gem "activesupport", require: "active_support"
gem "rspec", require: "rspec/autorun"
end
class Authenticator
DIGEST_ALGORITHM = "SHA256".freeze
KEY_LENGTH = 32
CODE_LENGTH = 10
def initialize(session, encryptor: self.class.encryptor)
@session = session
@encryptor = encryptor
end
def generate_token
code = self.class.generate_code
@session[:token_digest] = digest(code)
@encryptor.encrypt code
end
def authentic_code?(code)
@session[:token_digest] == digest(code)
end
def authentic_token?(token)
authentic_code? decrypt(token)
end
def decrypt(encrypted_token)
@encryptor.decrypt(encrypted_token)
end
def delete
@session.delete :key
@session.delete :token_digest
nil
end
def self.encryptor
@encrypter ||= if defined? Rails
TokenEncrypter.new(secret_key: Rails.application.credentials)
else
TokenEncrypter.new
end
end
private
def key
@session[:key] ||= random_token
end
def digest(data)
OpenSSL::HMAC.hexdigest(DIGEST_ALGORITHM, key, data)
end
def random_token
SecureRandom.hex(KEY_LENGTH)
end
def self.generate_code
Anybase::Base62.random(CODE_LENGTH)
end
end
class TokenEncrypter
SECRET_KEY_LENGTH = 32
def initialize(secret_key: self.class.generate_secret_key)
@encryptor = ActiveSupport::MessageEncryptor.new(secret_key)
end
# Encrypts and then encodes token to make it URL-safe
def encrypt(token)
encrypted_token = @encryptor.encrypt_and_sign(token)
url_safe_encoded_token = Base64.urlsafe_encode64(encrypted_token)
url_safe_encoded_token
end
# Decodes and then decrypts token
def decrypt(token)
decoded_token = Base64.urlsafe_decode64(token)
@encryptor.decrypt_and_verify(decoded_token)
end
def self.generate_secret_key
SecureRandom.bytes(SECRET_KEY_LENGTH)
end
end
describe Authenticator do
let(:session) { { name: "Alice's Browser" } }
let(:other_session) { { name: "Bob's Browser" } }
let(:alice) { Authenticator.new(session) }
let(:bob) { Authenticator.new(other_session) }
let(:token) { alice.generate_token }
context "#authentic_token?" do
it "is authentic in Alice's browser" do
expect(alice).to be_authentic_token token
end
it "is not authentic in Bobs's browser" do
expect(bob).to_not be_authentic_token token
end
end
context "#authentic_code?" do
it "is authentic in Alice's browser" do
expect(alice).to be_authentic_code alice.decrypt(token)
end
it "is not authentic in Bobs's browser" do
expect(bob).to_not be_authentic_code alice.decrypt(token)
end
end
describe "token generation" do
it "generates different tokens each time" do
expect(alice.generate_token).to_not eql alice.generate_token
end
end
describe "identical codes" do
let(:code) { "SAMECODE" }
before do
allow(Authenticator).to receive(:generate_code).and_return(code)
end
it "generates different tokens" do
expect(bob.generate_token).to_not eql alice.generate_token
end
describe "#authentic_code?" do
before do
bob.generate_token
alice.generate_token
end
it "is authentic in Alice's browser" do
expect(alice).to be_authentic_code code
end
it "is authentic in Bobs's browser" do
expect(bob).to be_authentic_code code
end
end
describe "#authentic_token?" do
it "is authentic in Alice's browser" do
expect(alice).to be_authentic_token alice.generate_token
end
it "is authentic in Bobs's browser" do
expect(bob).to be_authentic_token bob.generate_token
end
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment