jarvism

jarvism

Hello!
I have a Phoenix 1.7 LiveView app up and running with auth via
mix phx.gen.auth Accounts User users.
I’m still learning, but really enjoying it so far!
I’m running into a situation, though, where I am not quite sure even how to ask. Basically, I want to allow API users (outside of the LiveView) to access protected routes.
If I try to POST to /users/log_in, I get CSRF errors, which I think is correct. So, what is the correct way to get a token to use with the API?
Should I

  1. use mix phx.gen.secret to
  2. add an API key table with the user ID as a foreign key
  3. build my own plug to check the API key
  4. use Phoenix.Token to return a token if the API key is in the table?

I feel like I’m probably missing something since this must be a common use case with a canonical implementation.
Please point me in the right direction.
Thank you!

Showing Posts 1 to 8

mindok

mindok

I’m not that qualified in this area, and our authentication system was built on the back of Ash Authentication rather than using phx.gen.auth. However, the following may give you some pointers:

  1. In your router you need an API pipeline that bypasses the CSRF checks. A default Phoenix project should already have an :api pipeline.

  2. You would then expose a controller through this API pipeline to retrieve a token, e.g.

  scope "/api", MyWeb do
    pipe_through [:api]

    post "/get_token", AuthApiController, :get_token
  end

This wouldn’t include the controllers that expose functionality you want protected behind auth.

  1. Update your :api pipeline to attempt authentication, e.g. by reading a bearer token from the HTTP headers, checking that token, if successful, setting a :current_user assign on the connection. I’m not sure how you do that with phx.gen.auth. Ash Authentication has a load_from_bearer plug that handles most of the mechanics for us.

Our API pipeline looks more or less like:

  pipeline :api do
    plug :accepts, ["json"]
    plug :load_user_from_bearer_token # Try to load user from token
  end
  1. Add an auth check step to the protected routes, e.g.
  scope "/" do
    # The auth check is tacked onto the end of the pipeline
    pipe_through [:api, AuthApiPlug.api_user_required] 

    post "/sensitive_api/secret_thingz", SecretThingController, :create
  end

The AuthApiPlug.api_user_required function plug looks like this:

  def api_user_required(conn, _opts) do
    current_user = conn.assigns[:current_user]

    if is_struct(current_user, User) do
      conn
    else
      payload =
        %{
          errors: [
            %{
              message: "Access denied. Authentication required."
            }
          ]
        }
        |> Jason.encode!()

      conn
      |> put_resp_content_type("application/json")
      |> send_resp(401, payload)
      |> halt()
    end
  end

Hope this at least helps you know what to ask!!

jarvism

jarvism OP

@mindok Thank you so much for this!
I really appreciate the step-by-step walk-through.
I should have included in the original post that I have :api scope and pipeline. I think I’m falling down on the controllers (still learning Elixir).
That being said, it’s good to see this and reinforce the mental model, you know? So, thank you for verifying the basic architecture of:
1. Have a public /api/get_token route
2. Use the token obtained from /api/get_token for calls to private API routes.

Your step 3 also verifies what I can see in the docs about using a bearer token in headers, assigning :current_user (even outside of UI). So, thanks for verifying again. This is helpful.

Finally, the cherry on top is the plug code. I wasn’t sure if I should really be trying to hand-roll my own plug, so this is very helpful.

I think I have enough to move forward with a high-level understanding of what’s needed, muddle through the controller code, the plug code (thanks again for the example), and to be able to ask more specific questions as I run into problems.

Greatly, greatly appreciated!

codeanpeace

codeanpeace

It’s definitely worth reading through all the code generated by mix phx.gen.auth since it was designed to be very modular and extensible. For example, some of the functions that get added to your auth context could also be re-used in your API auth workflow and/or serve as a starting point for writing API auth-specific code.

Plugs are very much meant to be hand-rolled for encapsulating shared logic. If you haven’t already, check out this section of the Phoenix guides on Plugs as composition.

jarvism

jarvism OP

Thank you, @codeanpeace !

It’s definitely worth reading through all the code generated by mix phx.gen.auth

Yes! I have been trying to do this. Still a fairly steep learning curve, but getting better. But it seems obvious that I should be able to either ride on top of some of that code, repurpose it, or use it as example code. I will dive back in and give it a closer look.

Thank you for the “Plugs as composition” link (and general sense that, yes, these are meant to be written, not just consumed)! I will dig in there as well. :nerd_face:

adw632

adw632

You can also look at this comprehensive tutorial by FullstackPhoenix on adding API auth using guardian to an app that is using phx.gen.auth

Another option is to look at pow, but that would basically replace phx.auth.gen.

Until Pow officially supports liveview I can’t recommend it for that use case, however it appears to be close.

Pow would require a bit of work and there will likely be edge cases (such as signing out of sockets) which you don’t want in an authentication solution. Perhaps @danschultzer can advise further.

jarvism

jarvism OP

Hey, @adw632 !
Thanks for this. I have seen Pow and Guardian in docs, but I assumed they were strictly alternatives that were superseded by phx.gen.auth. I will give Guardian a look!

arcanemachine

arcanemachine

I have a project that contains an implementation of what you’re describing:

https://github.com/arcanemachine/phoenix-todo-list

In short, you use the :api pipeline in your router instead of the :browser pipeline so you can bypass the CSRF stuff. Then, you make 2 custom plugs:

  1. A plug to fetch the current API user (based on the bearer token that they pass in the Authorization HTTP header)

  2. A plug or two to fetch the desired resource, and to verify that the user is eligible to access the protected resource.

It’s been a little while since I made this (like a couple months), but I believe I had to implement the bearer token auth myself. It uses the exact same auth token generation scheme as that used by the built-in auth generators, but repackages the values so that they can be delivered over JSON. I dunno, it’s all in the repo, and I documented and tested everything pretty well (no warranties but I’m quite certain I didn’t do anything stupid.).

There’s also an OpenAPI spec and client for it in on the live site for the project (it’s just a todo list CRUD API), so if you’re familiar with those, you can use the online client or import the spec into Postman or Insomnia or whatever. There’s also an Insomnia spec that I used for development which should work (you may need to tweak the environment variables).

It may not be a perfect implementation, but it should have the basics of what you’re looking for. Hope it helps.

EDIT: Looks like I posted pretty much everything that @mindok said :smiley:

adw632

adw632

This is also a good explanation on using guardian JWT for API auth.

One thing that is worth paying some attention to is token expiry. Best advice is to keep access tokens very short lived (e.g. 1 minute) and then you don’t need to store JWT’s in a database, look them up or even worry about supporting revocation in most cases.

You can do revocation if 1 minute is too big of a risk in a performant manner and still avoid managing tokens in the database which would then require a database lookup in the fast path and a background process to keep sweeping out the old tokens.

If you really, must do revocation, consider using mnesia for fast lookup and cluster wide state (doesn’t need to persist) or alternatively use ETS to maintain an in memory cache and notify other nodes with phoenix channels (similar to Phoenix sign out here). Only the userid and revocation time needs to be maintained and it only needs to exist over the next minute (or whatever your token expiry period is) to force reauthentication. On initial startup of your app, require all API clients to reauthenticate if their token is older than the start time of the VM, so as to close out a potential hole if the VM restarts within the 1 minute token lifetime.

Which also leads into the typical case where most apps will lookup the user from the database on every API request. This again puts the database in the API fast path which can be quite limiting to overall API performance.

If you want high performance then bundle all the necessary information required to make API access decisions, such as the user id and their role into the JWT (it’s a self contained unforgable data structure) and avoid the cost of database lookups on each API request.

If you have sensitive information you wish to put into the token but not allow the client to observe it then use a JWE (encrypted JWT). Guardian supports that here.

— All posts loaded —

Where Next? Top

Trending in Questions Top

Blokh
Hey guys, I’ve got a huge CSV ( around 10 GB ) that needs to be processed hourly Do you guys have any suggestions what is the best prac...
New
RSP87
I’m working on a project that simulates the bumbl example in the programming phoenix book. It acts almost like an email client. We have a...
New
kszambelanczyk
Hello! Could someone please give me a help/sample code, how to delete a file from s3 using waffle/waffle_ecto from Phoenix app. I creat...
New
RemyXRenard
I’m seeing that a list inside a Kino.DataTable will be interpreted as a charlist, even if the Kino.configure() is set to charlists: :as_l...
New
velrest
So my question is quite simple and i have found no conclusive answer on forum, google or AI. Should we use :erlang.float for Integer to ...
New
samoloth
Hi, I’ve just set up an application with ash_authentication. There is only magic link strategy for now, so there is no confirmation add o...
New
FlyingNoodle
If a change or preparation module uses Ash.Changeset.get_argument/2 or Ash.Query.get_argument/2 (or any of the other get_argument functio...
New

Other Trending Topics Top

mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
garrison
Hobbes is a low-level distributed database for the Elixir programming language. Hobbes provides a simple, safe, and scalable storage lay...
New
marciok
Hi there! We created Gust: A task orchestrator inspired by Airflow. For those who have never heard about Aiflow, it’s a Python-based wor...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge & Solve. They are GUI (Emerge) and State management (S...
New
netoum
Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and Live...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews