I’m following a guide on Guardian authentication with elixir, but the need for refresh tokens isn’t very clear to me. This is my current understanding:
- Access tokens allow a user to make secure calls to the API
- Access tokens are powerful, so they need to expire in a timely manner (15 minutes) in order to prevent malicious actors from stealing the token and using it
- In order to prevent the user from having to log in again to get a new access token we use a refresh token (which last longer, 7 days in my case) to regenerate the access token.
Why bother with refresh tokens if they can regenerate access tokens? It seems like misleading security. It’s just an extra step for the attacker. Instead of using the access token directly, he uses the stolen refresh token to generate a new access token.
Here is my session_controller in case it helps:
defmodule AuthTutorialPhoenixWeb.SessionController do
use AuthTutorialPhoenixWeb, :controller
alias AuthTutorialPhoenix.Accounts
alias AuthTutorialPhoenix.Guardian
action_fallback AuthTutorialPhoenixWeb.FallbackController
def new(conn, %{"email" => email, "password" => password}) do
case Accounts.authenticate_user(email, password) do
{:ok, user} ->
{:ok, access_token, _claims} =
Guardian.encode_and_sign(user, %{}, token_type: "access", ttl: {15, :minute})
{:ok, refresh_token, _claims} =
Guardian.encode_and_sign(user, %{}, token_type: "refresh", ttl: {7, :day})
conn
# refresh token is stored as a cookie
|> put_resp_cookie("ruid", refresh_token)
|> put_status(:created)
# access token is an artifact that clients can use to make secure calls to an API server
|> render("token.json", access_token: access_token)
{:error, :unauthorized} ->
body = Jason.encode!(%{error: "unauthorized"})
conn
|> send_resp(401, body)
end
end
def refresh(conn, _params) do
refresh_token =
Plug.Conn.fetch_cookies(conn)
|> Map.from_struct()
|> get_in([:cookies, "ruid"])
# refresh token is used to generate a new access token without needing the user to log in again
case Guardian.exchange(refresh_token, "refresh", "access") do
{:ok, _old_stuff, {new_access_token, _new_claims}} ->
conn
|> put_status(:created)
|> render("token.json", %{
access_token: new_access_token
})
{:error, _reason} ->
body = Jason.encode!(%{error: "unauthorized"})
conn
|> send_resp(401, body)
end
end
def delete(conn, _params) do
conn
|> delete_resp_cookie("ruid")
|> put_status(200)
|> text("Log out successful")
end
end
After some online searching, it seems that you should also need to provide some identification when using a refresh token. Am I doing that here, or is my implementation faulty?