team-alembic / ash_authentication

The Ash Authentication framework
MIT License
96 stars 52 forks source link

Error when manually calling generated `sign_in_with_token` function in User resource #821

Closed sevenseacat closed 3 weeks ago

sevenseacat commented 3 weeks ago

AshAuthentication version: 4.2.7 Ash version: 3.4.39

In my app I've set up AshAuthentication with the new igniters, and added the password strategy:

$ mix igniter.install ash_authentication
$ mix ash_authentication.add_strategy password

This added the following authentication block in my new User resource:

  authentication do
    tokens do
      enabled? true
      token_resource Tunez.Accounts.Token
      signing_secret Tunez.Secrets
    end

    strategies do
      password :password do
        identity_field :email

        resettable do
          sender Tunez.Accounts.User.Senders.SendPasswordResetEmail
        end
      end
    end

    add_ons do
      confirmation :confirm_new_user do
        monitor_fields [:email]
        confirm_on_create? true
        confirm_on_update? false
        sender Tunez.Accounts.User.Senders.SendNewUserConfirmationEmail
      end
    end
  end

And an action like this:

    read :sign_in_with_token do
      description "Attempt to sign in using a short-lived sign in token."
      get? true

      argument :token, :string do
        description "The short-lived sign in token."
        allow_nil? false
        sensitive? true
      end

      prepare AshAuthentication.Strategy.Password.SignInWithTokenPreparation

      metadata :token, :string do
        description "A JWT that can be used to authenticate the user."
        allow_nil? false
      end
    end

I'm attempting to call this action in iex, but it appears to error with any value I pass in as the token argument.

iex(5)> # user is a user record returned from `sign_in_with_password`
...(5)> user.__metadata__.token
"eyJhbGciOi..."
iex(6)> Tunez.Accounts.User
Tunez.Accounts.User
iex(7)> |> Ash.Query.for_read(:sign_in_with_token, %{token: user.__metadata__.token})
** (MatchError) no match of right hand side value: :error
    (ash_authentication 4.2.7) lib/ash_authentication/strategies/password/sign_in_with_token_preparation.ex:17: AshAuthentication.Strategy.Password.SignInWithTokenPreparation.prepare/3
    (ash 3.4.39) lib/ash/query/query.ex:756: anonymous fn/6 in Ash.Query.run_preparations/6
    (elixir 1.17.3) lib/enum.ex:4858: Enumerable.List.reduce/3
    (elixir 1.17.3) lib/enum.ex:2585: Enum.reduce_while/3
    (ash 3.4.39) lib/ash/query/query.ex:577: Ash.Query.for_read/4
    iex:7: (file)
iex(7)> Ash.Query.for_read(Tunez.Accounts.User, :sign_in_with_token, %{token: "any token"})
** (MatchError) no match of right hand side value: :error
    (ash_authentication 4.2.7) lib/ash_authentication/strategies/password/sign_in_with_token_preparation.ex:17: AshAuthentication.Strategy.Password.SignInWithTokenPreparation.prepare/3
    (ash 3.4.39) lib/ash/query/query.ex:756: anonymous fn/6 in Ash.Query.run_preparations/6
    (elixir 1.17.3) lib/enum.ex:4858: Enumerable.List.reduce/3
    (elixir 1.17.3) lib/enum.ex:2585: Enum.reduce_while/3
    (ash 3.4.39) lib/ash/query/query.ex:577: Ash.Query.for_read/4
    iex:7: (file)

Am I missing something in how this action needs to be called? AA can't seem to figure out that I have the password strategy configured and that's what strategy it should be using to verify the token?

zachdaniel commented 3 weeks ago

🤔 that is pretty strange, from what I can tell we are properly connecting the action name to the strategy, so that line should not return :error. With that said, there are a fair few places in this codebase that are doing that happy-path pattern matching that really need to be adjusted to raise some kind of semantic error. Even if it blows up, it should blow up with some kind of error that says...anything :)

zachdaniel commented 3 weeks ago

Ah, actually I think I see the issue.

zachdaniel commented 3 weeks ago

This should be fixed in main. Please LMK if not.

sevenseacat commented 3 weeks ago

I don't get the error anymore, which it good!

Now I get an actual Ash error when trying to use a valid token:

iex(15)> Ash.Query.for_read(Tunez.Accounts.User, :sign_in_with_token, %{token: user.__metadata__.token})
#Ash.Query<resource: Tunez.Accounts.User, arguments: %{token: "**redacted**"}>
iex(16)> |> Ash.read(authorize?: false)
{:error,
 %Ash.Error.Forbidden{
   bread_crumbs: ["Error returned from: Tunez.Accounts.User.sign_in_with_token"],
   query: "#Query<>",
   errors: [
     %AshAuthentication.Errors.AuthenticationFailed{
       caused_by: %{
         message: "The token purpose is not valid",
         ...

I might be misunderstanding how the flow is supposed to work. My understanding is that the user initially signs in with sign_in_with_password like on a login form, they get the record back with the token, and then that token is stored in the browser/client and sent with subsequent requests, which would use sign_in_with_token to authenticate?

I think I remember a discussion about "exchange a short lived token for a long lived token", so am I not supposed to be calling this action? Is it called automatically behind the scenes on sign-in, and the returned token is already a long-lived token so it only gets validated on subsequent requests?

zachdaniel commented 3 weeks ago

Correct, this is the action used for the "short lived sign in token", which is generated when a user signs in over liveview. We want to validate the username and password in the liveview for instant feedback w/o a page reload, but we can't do that and then redirect with user/pass to an endpoint because we don't want to put their credentials an the query string of a get request.

There is a different action for what you're looking to do. I forget what it is and I'm on mobile, so I will check and comment when I find it.

sevenseacat commented 3 weeks ago

Yeah I think for now I just want to validate the token, which I can do with AshAuthentication.Jwt.verify. The rest will come later when AAP gets hooked up.

zachdaniel commented 3 weeks ago

Actually, I'm going to bed :) @jimsynz can elaborate more but IIRC the flow is to extract the subject from the token and then there is a function somewhere like "subject_to_user" that gets that user. Since tokens are verified, we don't need to really "do" anything except look up the user they identify (unlike with the sign in tokens where we want to exchange the token for a regular one).