How to generate a random url safe string with Elixir

23,800

Solution 1

What you can do instead is to generate a Base64-encoded string to be used as a confirmation token. This confirmation token will then be saved to your DB and passed as params to the activation link. Your activation url would look something like:

activation_url(MyApp.Endpoint, :confirm, confirm_id: confirm_id)

The above url helper assumes you have a MyApp.ActivationController and a confirm/2 action in that controller. To generate the confirm_id, you could do:

def random_string(length) do
  :crypto.strong_rand_bytes(length) |> Base.url_encode64 |> binary_part(0, length)
end

# random_string(64)

In your MyApp.ActivationController.confirm/2, you could have code lik:

def confirm(conn, %{"confirm_id" => confirm_id}) do
  user = Repo.get_by(User, confirm_id: confirm_id)
  User.confirm(user)
  conn
  |> put_flash(:info, "Account confirmed!")
  |> redirect(to: "/")
end

Hope that helps!

Solution 2

You can easily define a module to do this. In this example, @chars determines what characters appear in your generated strings.

defmodule StringGenerator do
  @chars "ABCDEFGHIJKLMNOPQRSTUVWXYZ" |> String.split("")

  def string_of_length(length) do
    Enum.reduce((1..length), [], fn (_i, acc) ->
      [Enum.random(@chars) | acc]
    end) |> Enum.join("")
  end
end

StringGenerator.string_of_length(3) # => "YCZ"

Solution 3

As noted in @JimGray's comment, your specification should really be in terms of the amount of entropy you want to represent by the random URL safe strings. Something along the lines of "I need N bits" because someone told you to use N bits, or "I want to avoid repeat in N strings and I can accept a risk of 1 in n of a collision". Either way, it's directly about entropy and only indirectly about string length.

For example, be sure that if you use a solution like @Gjaldon' answer you understand even though 512 bits of randomness is used, the amount of entropy for the actual string generated by random_string(64) is 320 bits. Whether that's sufficient is of course dependent on your scenario, which as noted above is probably best expressed as, for example, "I need a million strings with no more than a 1 in a trillion risk of repeat", in which case 320 bits is gross overkill as you'd only need 79.

If you want more control and understanding of generating random strings, look at EntropyString. With that library, you could do something like the following to get a string with 256 bits of entropy:

iex> defmodule Id, do: use EntropyString, charset: charset64
iex> Id.token
"ziKYK7t5LzVYn5XiJ_jYh30KxCCsLorRXqLwwEnZYHJ"

Or if you realize a million strings with a repeat risk of 1 in a trillion is sufficient, you could set up your Id generation like:

iex> defmodule Id do
...>   use EntropyString, charset: charset64
...>   @bits entropy_bits(1.0e6, 1.0e12)
...>   def random, do: Id.random_string(@bits)
...> end
iex> Id.random
"FhlGVXOaXV9f3f"

Either way, control and understanding are nice things to have.

Share:
23,800

Related videos on Youtube

NoDisplayName
Author by

NoDisplayName

Updated on November 16, 2020

Comments

  • NoDisplayName
    NoDisplayName over 3 years

    I need to be able to generate random url safe strings so I could use those in links (like in an activation link sent to a user's email), so how can I generate it? Is there a way to do that only with Elixir or I'd have to use some library?

  • NoDisplayName
    NoDisplayName over 8 years
    Will the result of random_string/1 be url safe?
  • Gjaldon
    Gjaldon over 8 years
    I updated the answer to use the url-safe Base64 version.
  • Jim Gray
    Jim Gray about 8 years
    Side note: you often want a target number of random bits rather than string length, e.g. session tokens should be at least 256 bit (32 bytes). To accomplish this, the binary_part() call should be omitted, at which point length will be the entropy of the generated key in bytes.
  • Sven Koschnicke
    Sven Koschnicke about 7 years
    Note that @chars also contains the empty string which means that the generated string may be shorter than length. To get only strings of one character use String.split("", trim: true).
  • Joe Freeman
    Joe Freeman over 6 years
    Thanks for this. Think you could alternatively just use String.codepoints instead of String.split. Also could be a bit cleaner using map, fwiw: 1..length |> Enum.map(fn _i -> Enum.random(@chars) end) |> Enum.join("").