Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

API integration guide #176

Closed
danschultzer opened this issue May 1, 2019 · 14 comments · Fixed by #247
Closed

API integration guide #176

danschultzer opened this issue May 1, 2019 · 14 comments · Fixed by #247
Labels
enhancement New feature or request help wanted Extra attention is needed

Comments

@danschultzer
Copy link
Collaborator

There's been a few issues with questions on how to integrate Pow with an API built in Phoenix: #170 #167 #153

A guide on API integration should be written. While I believe many would use absinthe, I think the guide should just focus on using plain Phoenix.

There may be some changes that can be made to Pow to make it as easy as Pow currently is for browser pipeline setup for API integration. However, I have a feeling that it's better to be explicit with routes for API so requiring the developer to add the individual routes rather than having a pow_routes/0 or pow_api_routes/0 macro. Sessions can be used as API keys, but it may be better to show how to use Phoenix.Token instead.

@popo63301
Copy link
Contributor

That would be great to have ! 😄

@danschultzer
Copy link
Collaborator Author

danschultzer commented Aug 8, 2019

Here's an example of an API auth plug with renewal logic commented out:

defmodule MyAppWeb.Pow.Plug do
  use Pow.Plug.Base

  alias Plug.Conn
  alias Pow.Config

  @impl true
  @spec fetch(Conn.t(), Config.t()) :: {Conn.t(), map() | nil}
  def fetch(conn, config) do
    token = fetch_auth_token(conn)

    config
    |> store_config()
    |> Pow.Store.CredentialsCache.get(token)
    |> case do
      :not_found -> {conn, nil}
      user       -> {conn, user}
    end
  end

  @impl true
  @spec create(Conn.t(), map(), Config.t()) :: {Conn.t(), map()}
  def create(conn, user, config) do
    token = Pow.UUID.generate()
    # renew_token = Pow.UUID.generate()
    conn  = Conn.put_private(conn, :pow_auth_token, token)
    # conn = Conn.put_private(conn, :pow_auth_renew_token, renew_token)

    config
    |> store_config()
    |> Pow.Store.CredentialsCache.put(token, user)

    # config
    # |> store_config()
    # |> PowPersistentSession.Store.PersistentSessionCache.put(renew_token, user.id)

    {conn, user}
  end
  
  @impl true
  @spec delete(Conn.t(), Config.t()) :: Conn.t()
  def delete(conn, config) do
    token = fetch_auth_token(conn)

    config
    |> store_config()
    |> Pow.Store.CredentialsCache.delete(token)

    conn
  end
  
  # @doc """
  # Create a new token with the provided authorization token.
  #
  # The renewal authorization token will be deleted from the store after the user id has been fetched.
  # """
  # @spec renew(Conn.t(), Config.t()) :: {Conn.t(), map() | nil}
  # def renew(conn, config) do
  #   renew_token = fetch_auth_token(conn)

  #   store_config = store_config(config)
  #   user_id = PowPersistentSession.Store.PersistentSessionCache.get(store_config, renew_token)
  #   PowPersistentSession.Store.PersistentSessionCache.delete(store_config, renew_token)
  #
  #   load_and_create_session(user_id, conn, config)
  # end
  
  # defp load_and_create_session(:not_found, conn, _config), do: {conn, nil}
  # defp load_and_create_session(id, conn, config) do
  #   case load_user(id) do
  #     nil -> {conn, nil}
  #     user -> create(conn, user, config)
  #   end
  # end

  defp fetch_auth_token(conn) do
    conn
    |> Plug.Conn.get_req_header("authorization")
    |> List.first()
  end
  
  defp store_config(config) do
    backend = Config.get(config, :cache_store_backend, Pow.Store.Backend.EtsCache)

    [backend: backend]
  end
end

So I imagine that a dev just sets up the API routes:

  pipeline :api do
    plug MyAppWeb.Pow.Plug, otp_app: :my_app
  end

  pipeline :browser do
    plug Pow.Plug.Session, otp_app: :my_app
  end

And then deals with the the renewal method directly in the controllers:

defmodule MyAppWeb.API.AuthController do
  use MyAppWeb, :controller

  alias MyAppWeb.Pow.Plug, as: AuthPlug

  def create(conn, %{"user" => user_params}) do
    conn
    |> Pow.Plug.authenticate_user(user_params)
    |> case do
      {:ok, conn} -> # display success response and pass auth token
      {:error, conn} -> # display error response
    end
  end

  def delete(conn, _params) do
    {:ok, conn} = Pow.Plug.clear_authenticated_user(conn)

    # display success response
  end

  def renew(conn, _params) do
    config = Pow.Plug.fetch_config(conn)

    case AuthPlug.renew(conn, config) do
      {conn, nil} -> # display error response
      {conn, _user} -> # display success response and pass auth token
    end
  end
end

@lud
Copy link

lud commented Jan 11, 2020

Hi,

I will not create a new issue because I only have questions, but I guess the Pow.Plug.Base docs could be improved.

I want to expose a route with API key auth, but I do not need any session, just accept the request, check the API key header and proceed. I am not sure what I should implement in the create and delete callbacks of the behaviour.

@danschultzer
Copy link
Collaborator Author

If you don't need to store the API key anywhere then you can take a look at the example in the readme using Phoenix.Token: https://github.com/danschultzer/pow#authorization-plug

You just have to replace all the Plug session stuff there. create should assign the API key so it can be rendered, and delete won't do a thing since the token is stateless.

You are right that the docs for Pow.Plug.Base could be improved. Let me see how I can expand on it with a short working example.

@lud
Copy link

lud commented Jan 11, 2020

In my case it is the opposite, I have API keys stored in database, so I am implementing the fetch callback to retrieve the user that owns it. Should I implement create instead and return a nil user from fetch ?

@lud
Copy link

lud commented Jan 11, 2020

All I did for now is the following:

  def create(_, _, _) do
    raise "called create"
  end

  def delete(_, _) do
    raise "called delete"
  end

  def fetch(conn, _config) do
    case fetch_auth_token(conn) do
      nil ->
        {conn, nil}

      token ->
        IO.inspect(token, label: "token")

        case Revamp.Repo.get_by(ApiKey, keyval: token) do
          nil ->
            {conn, nil}

          %ApiKey{} = api_key ->
            user = Ecto.assoc(api_key, :user)
            conn = Conn.put_private(conn, :revamp_api_key_is_write, api_key.is_write)
            {conn, user}
        end
    end
  end

  defp fetch_auth_token(conn) do
    conn
    |> Plug.Conn.get_req_header("authorization")
    |> List.first()
  end

I have two kind of keys, the "read" keys that will be visible in the html/javascript of client applications, stored in plaintext, and the "write" keys that allows the request to change data.

Should I implement hashing with bcrypt on my own for these ?

@danschultzer
Copy link
Collaborator Author

create/3 and delete/2 are used when the user signs in and out, but I'm unsure what you attempt to achieve with the plug. If you only need to check the API key for certain routes then you may not need to handle it with Pow at all.

I've updated your example with some more logic to give you a better idea how it should work:

defmodule MyAppWeb.Pow.Plug do
  use Pow.Plug.Base
  
  alias Plug.Conn
  alias MyApp.ApiKeys

  @impl true
  def fetch(conn, _config) do
    conn
    |> fetch_auth_token()
    |> load_key()
    |> case do
      nil ->
        {conn, nil}

      key ->
        conn = Conn.put_private(conn, :revamp_api_key_is_write, key.is_write)

        {conn, key.user}
    end
  end

  defp fetch_auth_token(conn) do
    conn
    |> Plug.Conn.get_req_header("authorization")
    |> List.first()
  end
  
  defp load_key(nil), do: nil
  defp load_key(token), do: ApiKeys.get_by_keyval(token)

  @impl true
  def create(conn, user, _config) do
    conn =
      case ApiKeys.create(user) do
        {:ok, key}           -> Conn.put_private(conn, :api_key, key)
        {:error, _changeset} -> conn
      end

    {conn, user}
  end

  @impl true
  def delete(conn, _config) do
    conn
    |> fetch_auth_token()
    |> load_key()
    |> invalidate_key()
    
    conn
  end
  
  defp invalidate_key(nil), do: nil
  defp invalidate_key(key), do: ApiKeys.invalidate(key)
end

defmodule MyApp.KeyApis do
  alias MyApp.{ApiKeys.ApiKey, Repo}
  
  import Ecto.Query

  def get_by_keyval(token) do
    query = from k in ApiKey, where: is_nil(k.invalidated_at) and k.token == ^token
    
    Repo.one(query)
  end
  
  def invalidate(key) do
    key
    |> ApiKey.changeset_delete()
    |> Repo.update()
  end
  
  def create(user) do
    key
    |> ApiKey.changeset(%{user: user})
    |> Repo.insert()
  end
end

@lud
Copy link

lud commented Jan 12, 2020

So I understand in my case that create() and delete() would actually create or delete the api key in the database. So yes I do not need them.

Using the plug may be unessecary but it keeps my code clean and declarative, relying on pow to call the error handler if needed or to put the user in session, I just have to write a function to read the key and maybe return a user.

Thank you for your time.

@danschultzer
Copy link
Collaborator Author

Yup, you don't need create and delete in this case, and omitting them or raising an error is what you should do. It won't be triggered unless you use the plug to sign in users with their credentials, or log out.

@lud
Copy link

lud commented Jan 13, 2020

Thanks !

@eamonpenland
Copy link

does anyone have an example of the PowInvitation extension with an api?

@danschultzer
Copy link
Collaborator Author

@eamonpenland I don't know of any example, but there's plans for a full API guide (though only for PowEmailConfirmation and PowResetPassword currently): pow-auth/pow_site#14

@eamonpenland
Copy link

eamonpenland commented Mar 29, 2020 via email

@danschultzer
Copy link
Collaborator Author

Sure!

Please push it to the pow_site repo, as it's out of scope for the Pow docs. A comment/gist/repo is also fine 😄 (as seen in pow-auth/pow_site#14 with a repo containing the guides for the other two extension)

I would like to put all of them together and turn it into a complete A-Z guide for API integration.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request help wanted Extra attention is needed
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants