Filling encrypted fields during user registration

I'm using Cloak to encrypt some fields of my user resource. I have a very similar setup as the one used in AshHQ. The field itself is configured in that way in the resource:
attributes do
...

attribute :encrypted_phone_number, Marketplace.Types.EncryptedString do
allow_nil? false
private? true
end
end

calculations do
calculate :phone_number, :string, {Marketplace.Calculations.Decrypt, field: :encrypted_phone_number}
end
attributes do
...

attribute :encrypted_phone_number, Marketplace.Types.EncryptedString do
allow_nil? false
private? true
end
end

calculations do
calculate :phone_number, :string, {Marketplace.Calculations.Decrypt, field: :encrypted_phone_number}
end
Now, the problem is how to make the ash_authentication action register_with_password receive that value as input and insert into the DB with the rest of the arguments.
29 Replies
jart
jart3y ago
It might be better to implement your encryption in terms of the Ash.Type behaviour then the actions don't need to know anything about it. https://ash-hq.org/docs/module/ash/latest/ash-type
Ash HQ
Module: Ash.Type
View the documentation for Ash.Type on Ash HQ.
Blibs
BlibsOP3y ago
That is what I did actually:
defmodule Marketplace.Types.EncryptedString do
@moduledoc "Represents a string that is encrypted when cast as input"

use Ash.Type

@impl true
def storage_type, do: :binary

@impl true
def cast_input(nil, _), do: {:ok, nil}

def cast_input(value, _) do
Marketplace.Vault.encrypt(value)
end

@impl true
def cast_stored(nil, _), do: {:ok, nil}

def cast_stored(value, _) when is_binary(value) do
{:ok, value}
end

def cast_stored(_, _), do: :error

@impl true
def dump_to_native(nil, _), do: {:ok, nil}
def dump_to_native(value, _) when is_binary(value), do: {:ok, value}
def dump_to_native(_, _), do: :error
end
defmodule Marketplace.Types.EncryptedString do
@moduledoc "Represents a string that is encrypted when cast as input"

use Ash.Type

@impl true
def storage_type, do: :binary

@impl true
def cast_input(nil, _), do: {:ok, nil}

def cast_input(value, _) do
Marketplace.Vault.encrypt(value)
end

@impl true
def cast_stored(nil, _), do: {:ok, nil}

def cast_stored(value, _) when is_binary(value) do
{:ok, value}
end

def cast_stored(_, _), do: :error

@impl true
def dump_to_native(nil, _), do: {:ok, nil}
def dump_to_native(value, _) when is_binary(value), do: {:ok, value}
def dump_to_native(_, _), do: :error
end
jart
jart3y ago
oh cool.
Blibs
BlibsOP3y ago
It is just that if I send the phone_number in the register_with_password action, it will return an error because encrypted_phone_number is required I think I can fix that with the changes code block, I'm gonna try that now
jart
jart3y ago
ah yeah right that makes sense yeah, I think adding a change is the way to go. either using changes or by generating the :register_with_password action yourself - ash_authentication will spit out errors for missing changes and attributes so you can just add each thing it complains about and it will guide you through it
Blibs
BlibsOP3y ago
Yeah, I'm not sure if I can do that with changes since I can't pass the phone_number value to it
jart
jart3y ago
can you make the phone number attribute not required and then validate it's presence on the update action or wherever you plan on setting it?
Blibs
BlibsOP3y ago
You mean making it not required and then setting it in another call outside of the regiester_with_password action?
jart
jart3y ago
yes either that or you need to build a registration form that provides the information you need to complete registration.
ZachDaniel
ZachDaniel3y ago
The UI components are more like a “quick start”, but if you have more fields/alterations to the sign in flow you’ll basically need to build your own pages
Blibs
BlibsOP3y ago
I'm not using the UI actually, I'm just running the action directly from my resource
jart
jart3y ago
oh. I misunderstood. why are you not able to just pass the phone number parameter when you do the registration then?
Blibs
BlibsOP3y ago
a = %{email: "[email protected]", password: "12345678", password_confirmation: "12345678", phone_number: "2131312"}

Marketplace.Accounts.User.register_with_password a
a = %{email: "[email protected]", password: "12345678", password_confirmation: "12345678", phone_number: "2131312"}

Marketplace.Accounts.User.register_with_password a
Like this I think because that field actually do not exists, it is a calculation, and at the same time the real field, encrypted_phone_number is a private field since I do not want it to leak, so for some reason I can pass it directly too
jart
jart3y ago
right so you need there to be an additional argument your best bet is to define your own :register_with_password action
Blibs
BlibsOP3y ago
What I did in ecto was have a step in my changeset that would get the phone number, encrypt it and put in the encrypted_phone_number field.
jart
jart3y ago
yeah that's basically what we do with the password hashing
Blibs
BlibsOP3y ago
That's a bummer. I will try that. I would expect that there would be a way to expand the action like the changes block but receiving the action inputs as a parameter 🤔
ZachDaniel
ZachDaniel3y ago
Well, you can but the thing you can’t do is add arguments to the generated action
jart
jart3y ago
if you define create :register_with_password do; end and run mix compile it will tell you what you need to add.
Blibs
BlibsOP3y ago
Not sure what you mean with that, if I just add this and run compile I just get a List.to_string error
jart
jart3y ago
what I mean is just define an empty :register_with_password action in your resource's action block and you should get compilation errors telling you what needs to be set. Can you paste the compile error please?
Blibs
BlibsOP3y ago
jart
jart3y ago
okay that's weird. it's caused by ash_graphql it's bug in ash graphql it should emit this:
raise Spark.Error.DslError,
module: resource,
message: """
No such action #{query_or_mutation.action} of type #{type} on #{inspect(resource)}
Available #{type} actions:
#{available_actions}
"""
raise Spark.Error.DslError,
module: resource,
message: """
No such action #{query_or_mutation.action} of type #{type} on #{inspect(resource)}
Available #{type} actions:
#{available_actions}
"""
Blibs
BlibsOP3y ago
Oh, damn... I jusr realised that I added the code inside the graphql block instead of the actions... Any idea why it is complaining about I'm having to pass hashed_password as an nullable input?
== Compilation error in file lib/marketplace/accounts/user.ex ==
** (Spark.Error.DslError) [Marketplace.Accounts.User]
actions -> allow_nil_input:
Expected the action `:register_with_password` to allow nil input for the field `:hashed_password`
(spark 0.4.1) lib/spark/dsl/extension.ex:605: Spark.Dsl.Extension.raise_transformer_error/2
(elixir 1.14.0) lib/enum.ex:4751: Enumerable.List.reduce/3
(elixir 1.14.0) lib/enum.ex:2514: Enum.reduce_while/3
(stdlib 4.1.1) erl_eval.erl:744: :erl_eval.do_apply/7
(stdlib 4.1.1) erl_eval.erl:961: :erl_eval.expr_list/7
(stdlib 4.1.1) erl_eval.erl:454: :erl_eval.expr/6
(stdlib 4.1.1) erl_eval.erl:136: :erl_eval.exprs/6
(stdlib 4.1.1) erl_eval.erl:282: :erl_eval.expr/6
== Compilation error in file lib/marketplace/accounts/user.ex ==
** (Spark.Error.DslError) [Marketplace.Accounts.User]
actions -> allow_nil_input:
Expected the action `:register_with_password` to allow nil input for the field `:hashed_password`
(spark 0.4.1) lib/spark/dsl/extension.ex:605: Spark.Dsl.Extension.raise_transformer_error/2
(elixir 1.14.0) lib/enum.ex:4751: Enumerable.List.reduce/3
(elixir 1.14.0) lib/enum.ex:2514: Enum.reduce_while/3
(stdlib 4.1.1) erl_eval.erl:744: :erl_eval.do_apply/7
(stdlib 4.1.1) erl_eval.erl:961: :erl_eval.expr_list/7
(stdlib 4.1.1) erl_eval.erl:454: :erl_eval.expr/6
(stdlib 4.1.1) erl_eval.erl:136: :erl_eval.exprs/6
(stdlib 4.1.1) erl_eval.erl:282: :erl_eval.expr/6
This is what I have so far:
create :register_with_password do
accept [:email]

argument :password, :string do
allow_nil? false
end

argument :password_confirmation, :string do
allow_nil? false
end

argument :phone_number, :string do
allow_nil? false
end

validate confirm(:password, :password_confirmation)

change AshAuthentication.Strategy.Password.HashPasswordChange
end
create :register_with_password do
accept [:email]

argument :password, :string do
allow_nil? false
end

argument :password_confirmation, :string do
allow_nil? false
end

argument :phone_number, :string do
allow_nil? false
end

validate confirm(:password, :password_confirmation)

change AshAuthentication.Strategy.Password.HashPasswordChange
end
All right! Now it is working, here is the final solution:
create :register_with_password do
accept [:email]

argument :password, :string do
allow_nil? false
sensitive? true
end

argument :password_confirmation, :string do
allow_nil? false
sensitive? true
end

argument :phone_number, :string do
allow_nil? false
end

argument :hashed_password, :string do
allow_nil? true
end

validate AshAuthentication.Strategy.Password.PasswordConfirmationValidation

change set_attribute(:encrypted_phone_number, arg(:phone_number))

change AshAuthentication.GenerateTokenChange
change AshAuthentication.Strategy.Password.HashPasswordChange
end
create :register_with_password do
accept [:email]

argument :password, :string do
allow_nil? false
sensitive? true
end

argument :password_confirmation, :string do
allow_nil? false
sensitive? true
end

argument :phone_number, :string do
allow_nil? false
end

argument :hashed_password, :string do
allow_nil? true
end

validate AshAuthentication.Strategy.Password.PasswordConfirmationValidation

change set_attribute(:encrypted_phone_number, arg(:phone_number))

change AshAuthentication.GenerateTokenChange
change AshAuthentication.Strategy.Password.HashPasswordChange
end
I'm still not sure why I need to set the hashed_password as an argument though.
jart
jart3y ago
What does the authentication dsl for the password strategy config look like?
Blibs
BlibsOP3y ago
Not sure if you meant this:
authentication do
api Marketplace.Accounts

strategies do
password :password do
identity_field :email

resettable do
sender Senders.SendPasswordResetEmail
end
end
end

tokens do
enabled? true

store_all_tokens? true
require_token_presence_for_authentication? true

token_resource Marketplace.Accounts.Token

signing_secret fn _, _ ->
Application.fetch_env(:marketplace, :token_signing_secret)
end
end

add_ons do
confirmation :confirm do
monitor_fields [:email]

sender Senders.SendConfirmationEmail
end
end

postgres do
table "users"

repo Marketplace.Repo

migration_defaults roles: "[\"buyer\"]"
end

identities do
identity :unique_email, [:email] do
eager_check_with Marketplace.Accounts
end
end
end
authentication do
api Marketplace.Accounts

strategies do
password :password do
identity_field :email

resettable do
sender Senders.SendPasswordResetEmail
end
end
end

tokens do
enabled? true

store_all_tokens? true
require_token_presence_for_authentication? true

token_resource Marketplace.Accounts.Token

signing_secret fn _, _ ->
Application.fetch_env(:marketplace, :token_signing_secret)
end
end

add_ons do
confirmation :confirm do
monitor_fields [:email]

sender Senders.SendConfirmationEmail
end
end

postgres do
table "users"

repo Marketplace.Repo

migration_defaults roles: "[\"buyer\"]"
end

identities do
identity :unique_email, [:email] do
eager_check_with Marketplace.Accounts
end
end
end
jart
jart3y ago
thanks I see that I added a validation for it, but I don't know why https://github.com/team-alembic/ash_authentication/blob/main/lib/ash_authentication/strategies/password/transformer.ex#L156:L156 the weird thing is that the built action doesn't contain that argument, so it should theoretically fail that validation @Blibs just released ash_authentication v3.9.2 which removes that validation.
Blibs
BlibsOP3y ago
Thanks @jart that did it!
jart
jart3y ago
my pleasure

Did you find this page helpful?