As I understand it, the philosophical design of Elixir is that it is function driven. This is partly what makes it so multi-threaded and efficient. Thus we must go out of our way to save “state” only when truly needed (ie. via GenServer or other modules that support state).
How do we then avoid unnecessary repeated functions?
For example, here is a class I started for my token (Joken) purposes:
defmodule My.Token do
def get_custom_claims(userID, durationHrs) do
token_config_map = %{} #empty
# userID:
|> Joken.Config.add_claim("userID", fn -> userID end, &(&1 == userID))
# issuer:
|> Joken.Config.add_claim("iss", fn -> "The Issuer" end, &(&1 == "The Issuer"))
# expiry:
|> Joken.Config.add_claim("exp", fn -> Joken.CurrentTime.OS.current_time() + (durationHrs * 60 * 60) end, &(&1 < Joken.CurrentTime.OS.current_time() ) )
{status, token_claims} = Joken.generate_claims(token_config_map)
token_claims
end
def encode_and_sign_claims(token_claims) do
signer_priv = Joken.Signer.create("RS256", %{"pem" => My.Token.private_key}) #custom signer
{:ok, token, _} = Joken.encode_and_sign(token_claims, signer_priv)
token
end
def verify_token_pub(token) do
signer_pub = Joken.Signer.create("RS256", %{"pem" => My.Token.public_key}) #custom signer
Joken.verify_and_validate(%{}, token, signer_pub)
end
def private_key do
"""
-----BEGIN PRIVATE KEY-----
...
-----END PRIVATE KEY-----
"""
end
def public_key do
"""
-----BEGIN PUBLIC KEY-----
...
-----END PUBLIC KEY-----
"""
end
end
This can then be used as:
token_claims = My.Token.get_custom_claims("Carlos", 1)
token = My.Token.encode_and_sign_claims(token_claims)
{status, data} = My.Token.verify_token_pub(token)
However it does not seem exactly ideal.
In reality, what I think I would like to do is store the private_key
string and public_key
string in .pem files on disk, then once a day reload them from disk (in case I hot swap the files on the server while it is running)
But this would require storing the strings in state. If not, I would have to load the pem directly from disk every time (!). This then adds a real unnecessary bottleneck with the disk access.
Additionally, above I am constantly re-creating signer_pub
and signer_priv
, when similarly to the key strings, I might only want to create them if they don’t already exist or the strings change, or say if the day changes over time to refresh.
In C# or C++ I would:
- Create an instance or static class to manage the keys & signers.
- Have that static class or instance monitor the date with a timer, and on date change, run an event/function to update them.
- In update function, load new keys from disc to strings, and if different from what we have, recreate
signer_pub
andsigner_priv
. - Would likely all be done synchronous on same thread, but otherwise, put a bool to protect the signer creation and if any validation/ attempts occur during this process, repeat them some way in 1 second (when should be long since done).
I presume this could conceptually be done similarly in Elixir with a GenServer, storing the key strings and signer_pub
and signer_priv
inside state
. This GenServer could presumably also check the date periodically to see if must update and reload the keys from disk then (how if so?). But then we have a potential bottleneck where everything must run through that GenServer (or we must spawn a group of worker such GenServers).
So what is the general philosophy here?
What would be the correct general practical solution?
Thanks for any help.