Authorization and API endpoints permissions

I am looking for a way of using tokens through the Authorization header, by obviously validating them and fetching what user they belong to. Furthermore, I would like to somehow automate endpoint permissions so that I can i.e. let an user POST to an endpoint but deny GET access. How would I go on about implementing this?

How do you generally handle permissions? Like, having a system with users and many resources they can manage. I kinda like this approach by DIscord, is this viable or efficient or implementable on a lib you know or are using?

I am using guardian for this and there is a good example for configuring api and tokens here.

Once done, You can also secure your websocket with this config…

Sounds good, how would you implement resource-specific perms though? How about I want an user to have access to modify i.e. a forum post? From what I’ve seen Guardian would only let me decide whether or not an user would have access to the hypothetical /posts/:id endpoint no matter what the id is, how do I instead hande id or resource-specific access?

Doesn’t Guardian thus suit better browser access where you want specific users to access i.e. an administration /dashboard?

I’d have a special permission like edit_own_articles as part of some role and call a non-guardian related function from the service layer. Here is a nice article on this topic:

Looks like a good solution, do you think Canary would be a better fit though? It looks like they approach the same but Canary seems to be doing it in a nicer way.

I only used Guardian so I can’t compare them, Canary looks good though.

What happens in the example protocol implementation is basically the “non-guardian related function” I meant above :slight_smile:

1 Like

I use something canada-like but not canada. I would authorize, say, a ‘specific’ ID like:

    conn
    |> can(%Permissions.SomeModule.Post{action: :edit, id: id})
    ~> case do conn ->
      ...
    end

The permissions can get as detailed as I want.

Is there any particular reason why you have decided going this way? Is the way Canary works perhaps not a good idea long-term?

Would then, as a rough idea, using Guardian for token auth and user fetching and a custom module that grabs conn’s user and checks whether to deny access a good alternative? Would you mind as well extending a bit on how your approach works? :slight_smile:

Canary is course-grained only to the request level, and in many cases I need to do very fine grained checking, such as on individually returned database rows.

Guardian encodes permissions in it, if you are not using that feature then don’t use Guardian and instead just use Phoenix.Token.

Mostly I just expose a set of helper functions from my MyServer.Accounts.Perms module such as can/2, can?/2, logged_in/2, logged_in?/2 and a few others. They can take a variety of ‘environments’ (which can be a conn, socket, some other custom things) and extract the information they need out of that, then they look up the permission structure (a pre-processed structure generated from a host of database and LDAP information) in the ETS cache (lifetime of 1 minute, I’m using Cachex for this) and I use my PermissionEx library to test the individual permission I want to the overall processed structure. The ? variants return true/false, the non-? variant return either the environment that was passed in (allowing easy threading of things like conn in pipelines) or it returns an exception structure if not allowed (which is then handled by the system to set a last_path on the connection and redirect to a login).

2 Likes

Thanks for elaborating! :slight_smile: After messing arround with canary and canada I am guessing I would just go better by creating my own module.

I was now thinking of implementing a plug using Phoenix.Token that essentially signs and verifies tokens and compares them to an Ecto schema (or a linked database’s table) - to make sure a token is truly linked to an account and therefore fetch its data. From its docs ‘it is safe to store identification information’ is read, so I am also thinking of storing an id to the token just so I can grab the account directly and compare against its stored token directly (instead of looking up for a matching account by token).

Now, when it comes to checking perms in a canada-like way, I also liked your way of doing it. I might though figure out a way of pluging canada’s behavior like canary does but in a more custom way.

Anyway, thanks to everyone who has replied to this topic so far! I guess it’s now time to go and figure out things by myself!

That is precisely what I do! ^.^

# My plug that grabs it from a session in `conn`
  def plug_load_account_id(%Plug.Conn{} = conn, [] = _opts) do
    case MyServer.Auth.verify_login(conn) do
      %Plug.Conn{} = conn -> conn
      _ -> conn
    end
  end

# verify_login is:
  def verify_login(%Plug.Conn{}=conn) do
    with\
      token = Plug.Conn.get_session(conn, "account"),
      {:ok, {account_id, account_session}} <- verify("account", token, max_age: token_max_age()),
      true <- confirm_account_id_session?(account_id, account_session) do
      Logger.metadata(account_id: account_id)
      Logger.metadata(account_session: account_session)
      Plug.Conn.assign(conn, :account_id, account_id)
    else
      err -> normalize(err)
    end
  end
  def verify_login(token) when is_binary(token), do: verify("account_id", token, max_age: token_max_age())
  # Snip other heads
  def verity_login(token) do
    Logger.warn("Invalid token typed passed to verity_login of:  #{inspect token}")
    {:error, :invalid_token}
  end

# confirm_account_id_session? is:
  def confirm_account_id_session?(account_id, account_session) do
    case get_account_id_session(account_id, account_session) do
      nil -> false
      %DB.Account.Session{} -> true
    end
  end

# and get_account_id_session is:
  def get_account_id_session(account_id, account_session) do
    query =
      from s in DB.Account.Session,
      where: is_nil(s.removed_at) and
        ((s.id == ^account_id and s.token == ^account_session) or (s.id == ^account_session and s.token == ^account_id))
    Repo.one(query)
  end

# And so forth...
2 Likes