Last active
August 31, 2023 17:12
-
-
Save bradgessler/ac4a5809749a81e0570e4b0e8500a035 to your computer and use it in GitHub Desktop.
NoPassword II
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 "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