riverrun / phauxth

Not actively maintained - Authentication library for Phoenix, and other Plug-based, web applications
409 stars 21 forks source link

[FEATURE] Add refresh token support #104

Open riverrun opened 5 years ago

riverrun commented 5 years ago

Problem

It is common practice to extend the time between logins by issuing a new token when presented with an existing token, or to have a separate refresh token that requests a new access token.

There is a concern, as noted in #103, about unauthorized persons gaining extended access to a system if the access token is compromised.

Solution

Using refresh tokens, together with additional safeguards, should make it more difficult for an attacker to gain extended access in this way.

At the moment, this matter needs to be researched more thoroughly before deciding what kind of addition to the library / documentation is needed.

dsignr commented 3 years ago

@riverrun David, here's how I use refresh tokens right now w/ Phauxth. This file resides in my controllers folder inside API/auth. Maybe you can just add this to the generator for the API with slight changes:

defmodule XYZWeb.API.Auth.Token do
  @moduledoc """
  Custom token implementation using Phauxth.Token behaviour and Phoenix Token.
  """

  @behaviour Phauxth.Token

  alias Phoenix.Token
  alias XYZWeb.Endpoint

  @access_token_max_age 259200 #3 days
  @refresh_token_max_age 31536000 # 1 year
  @access_token_salt "ABC" # change this to a proper salt if you don't want to get pwned
  @refresh_token_salt "DEF" # change this to a proper salt if you don't want to get pwned

  def max_age(token_type \\ :access_token) do
    case token_type do
      :refresh_token -> @refresh_token_max_age
      _ -> @access_token_max_age
    end
  end

  @impl true
  def sign(data) do
    Token.sign(Endpoint, @access_token_salt, data, [])
  end

  # @impl true
  # Max age not supported for signing
  def sign(data, opts \\ [], token_type \\ :access_token) do
    case token_type do
      :access_token -> Token.sign(Endpoint, @access_token_salt, data, opts)
      :refresh_token -> Token.sign(Endpoint, @refresh_token_salt, data, opts)
    end
  end

  defp updated_opts(opts, new_max_age) do
     {_, new_opts} = Keyword.get_and_update(opts, :max_age, fn current_max_age ->
      {current_max_age, new_max_age}
    end)
    new_opts
  end

  # @impl true
  def verify(token, opts \\ [], token_type \\ :access_token) do
    case token_type do
      :access_token -> Token.verify(Endpoint, @access_token_salt, token, updated_opts(opts, @access_token_max_age))
      :refresh_token -> Token.verify(Endpoint, @refresh_token_salt, token, updated_opts(opts, @refresh_token_max_age))
    end
  end
end
dsignr commented 3 years ago

So a brief explanation of how I use the above. When access token expires, the client will send the refresh token as a Bearer token in the API call, if it's valid, my endpoint will generate a new access token which will be cached on the mobile app or wherever I'm consuming it. One thing to add here is refresh tokens should ONLY be generated once during the lifetime of a user using your app for maximum security. So, maybe the very first time they sign up or when they resent their account. It's a security violation if you keep generating a refresh token upon every renewal request. Only generate an access token for renew endpoints. Cheers.