46

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
  • 15,246
  • 12
  • 62
  • 98

3 Answers3

76

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
starball
  • 20,030
  • 7
  • 43
  • 238
Gjaldon
  • 5,534
  • 24
  • 32
  • 1
    Will the result of `random_string/1` be url safe? – NoDisplayName Aug 14 '15 at 06:49
  • 1
    I updated the answer to use the url-safe Base64 version. – Gjaldon Aug 14 '15 at 07:03
  • 1
    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. – Jim Gray Feb 25 '16 at 09:03
14

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"
Nathan Long
  • 122,748
  • 97
  • 336
  • 451
  • 4
    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)`. – Sven Koschnicke Feb 21 '17 at 12:19
  • 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("")`. – Joe Freeman Oct 16 '17 at 16:52
6

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.

dingo sky
  • 1,445
  • 17
  • 15