diff --git a/app/services/waste_carriers_engine/secure_token_service.rb b/app/services/waste_carriers_engine/secure_token_service.rb new file mode 100644 index 000000000..283dac1f2 --- /dev/null +++ b/app/services/waste_carriers_engine/secure_token_service.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require "securerandom" + +module WasteCarriersEngine + # SecureTokenService generates a random base58 string of length 24. + # + # SecureRandom::base58 is used to generate the 24-character unique tokens, so + # collisions are highly unlikely. + # + # The result will contain only alphanumeric characters except 0, O, I and l + # + # p SecureRandom.base58 #=> "4kUgL2pdQMSCQtjE" + # p SecureRandom.base58(24) #=> "77TMHrHJFvFDwodq8w7Ev2m7" + # + # Copied almost verbatim from + # https://github.com/robertomiranda/has_secure_token + class SecureTokenService < BaseService + BASE58_ALPHABET = ("0".."9").to_a + ("A".."Z").to_a + ("a".."z").to_a - %w[0 O I l] + + def run + SecureRandom.random_bytes(24).unpack("C*").map do |byte| + idx = byte % 64 + idx = SecureRandom.random_number(58) if idx >= 58 + BASE58_ALPHABET[idx] + end.join + end + + end +end diff --git a/spec/services/waste_carriers_engine/secure_token_service_spec.rb b/spec/services/waste_carriers_engine/secure_token_service_spec.rb new file mode 100644 index 000000000..216ef07ff --- /dev/null +++ b/spec/services/waste_carriers_engine/secure_token_service_spec.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require "rails_helper" + +module WasteCarriersEngine + RSpec.describe SecureTokenService do + + describe ".run" do + context "the return value" do + it "is a string" do + expect(described_class.run).to be_a(String) + end + + it "is 24 characters in length" do + expect(described_class.run.length).to eq(24) + end + + it "contains only alphanumeric characters except 0, O, I and l" do + expect(described_class.run).to match(/^[a-km-zA-HJ-NP-Z1-9]*$/) + end + end + + it "generates a different result each time it is called" do + results = [] + 10.times do + latest = described_class.run + + expect(results).not_to include(latest) + + results.push(latest) + end + end + end + end +end