Ash authentication on mobile

Hi again! I decided to tackle this tonight.

I am not sure I understand correctly. What exactly is user.__metadata__.token?

For now I’m just trying this from a test:

    email = "test@test.com"
    password = "supersecret"

    payload = %{
      "user" => %{
        "email" => email,
        "password" => password,
        "password_confirmation" => password
      }
    }

    conn = post(conn, ~p"/auth/user/password/register", payload)
    data = json_response(conn, 200))

But I do not understand why the token is present in the user in the metadata. I created a MyApp.Accounts.Token entity like the tutorial said, but now I understand that those are for server-side-only metadata about the tokens. Do they target that same token I found in my user struct metadata?

It seems so.

So now I would like the user to sign-up and then be able to sign-in from different machines and revoke their login from a specific machine.

In the AuthController I did this in the success callback:

    Token
    |> Ash.Changeset.for_create(:store_token,
      token: user.__metadata__.token,
      purpose: "hello world"
    )
    |> Accounts.create!()
    |> dbg()

I seems that the purpose is not relevant for Ash to work, though required. So I can add the machine based on the user agent or a custom header or field. And I can now write a basic test controller like this :smiley:

  def check(conn, params) do
    resp_payload =
      case conn.assigns do
        %{current_user: user} -> %{logged_in: true, as: user.email}
        _ -> %{logged_in: false}
      end

    conn
    |> put_status(200)
    |> json(resp_payload)
  end

And run my full test to see if it worked:

  test "a user can authenticate via API using email and password", %{conn: base_conn} do
    # Create a user from API

    email = "test@test.com"
    password = "supersecret"

    payload = %{
      "user" => %{
        "email" => email,
        "password" => password,
        "password_confirmation" => password
      }
    }

    conn = post(base_conn, ~p"/auth/user/password/register", payload)
    _data_signup = json_response(conn, 200)

    # Now sign in
    conn = post(base_conn, ~p"/auth/user/password/sign_in", payload)
    data_signin = json_response(conn, 200)

    # Try the API without the token
    conn = get(base_conn, ~p"/api/check")
    data_checkwo = json_response(conn, 200)
    assert false == data_checkwo["logged_in"]

    # Try with
    conn =
      base_conn
      |> Plug.Conn.put_req_header(
        "authorization",
        "Bearer #{Map.fetch!(data_signin, "bearer_token")}"
      )
      |> get(~p"/api/check")

    data_checkwith = json_response(conn, 200)
    assert true == data_checkwith["logged_in"]
    assert email == data_checkwith["as"]
  end

Now I realize that there is not really a question in my message :smiley: I’ll try to create another resource and have readable by the owner only.

I added this in my router:

  import AshAuthentication.Plug.Helpers

And the new plug here:


  pipeline :api do
    plug :accepts, ["json"]

    # Ash
    plug :load_from_bearer
    plug :set_actor, :user
  end

But in the controller Ash.get_actor is always nil. I have seen that the plug only sets it in conn.private.ash.actor. So I guess I should write a custom plug to forbid some api routes to be accessed without authentication. Is that how you would do it? I do not need any public data on those routes.

Or would you rather just always set an actor on the action, even if it is nil, and let the policies reject the calls?

Thank you for reading, sorry for that unstructured post :smiley: it took me a couple hours to write because I implemented the code along.