Pistrie

Pistrie

Do refresh tokens provide a false sense of security?

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?

Marked As Solved

NobbZ

NobbZ

Just as an additional note:

The refresh/access token distinction is not to save you from the tokens getting stolen.

It is against the user loosing their account on the authentication server.

Lets say you have a company and issue your access tokens with a couple of minutes and the refresh token with a month.

The worker leaves after a week, but still has a valid refresh token. Now that the account has been deactivated on the authentication server, they can not use the refresh token anymore to get a valid access token, despite the fact that the refresh token has not expired.

The same technique is used when you hit “log me out from all devices” in facebook or similar services. The long lived refresh token gets revoked by the auth server not accepted anymore when asking for a new access token.

This dual tokens are necessary, to avoid the consumer having to ping the authentication server again and again for every request, whether the authenticated user is still authentic. Or even worse: assume authenticity for a very long time…

Also Liked

dorgan

dorgan

What’s the intended use case though?

In this thread it’s kind of assumed that this is for a website, and in such case why not just leverage old-fashioned http-only cookie based authentication? Phoenix by default even provides auth generators and CSRF protection so you don’t have to deal with any of the tedious bits. The fact that your endpoints return JSON instead of an html document does not magically make it require some special authentication strategy, so why bother?

Now if the API is either:
A- Intended to be consumed by a first-party client like a mobile app
B- Intended to be consumed by third parties

Then yeah it makes sense to discuss this.

For A, cookies are just regular http headers so you can also leverage them for your first-party mobile client, there’s almost no need to deal with a different strategy here.

For B, there’s stuff like OAuth2, this is the use case where the stateless tokens, refresh tokens and all that dance makes the most sense, and how to store these tokens is the problem of the third party, not yours.

I might be missing something important though, but really just because you return JSON doesn’t mean you have to think of a complete different authentication strategy, it’s more about who’s the party that will consume that and still assume malicious third parties are trying to own your base.

Maybe @Exadra37 has some insights on this, if he doesn’t mind the ping :slight_smile:

hauleth

hauleth

Regular, stateful, sessions. Just have table with fields session_id, data, and expire_at. Generate session ID via :crypto.secure_random_bytes(32) encode however you want and call it a day. It will be the most secure, most obvious, and easiest to manage approach.

cc @Pistrie

If you want a one-time-token then Phoenix.Token will be much better choice than JWT. It is simple, versioned, signed token. The lack of configurability is enormous gain there as you cannot accidentally use incorrect set of keys/features and make it much harder to fallback to insecure algos. If you want something cross-platform then PASETO or similar would be probably best choice (I am working on BASETO which will use BARE instead of JSON to encode the data, but that is irrelevant there, the idea is the same).

For browsers sessions just use HTTP-Only cookies, for API you can use Cookies or Authorisation header, that doesn’t really change much. If you want to be ultra secure, then you can try mTLS for that, rarely used, but super powerful approach.

hauleth

hauleth

Yes, indeed. That is why using stateless tokens for sessions is dumb idea. You should not use JWTs for sessions and in general you should not use JWTs at all.

Where Next?

Popular in Questions Top

vertexbuffer
Hello, can anybody help here..? I have a list of players and I what to delete an element, but every for loop the list is reverting to ori...
New
9mm
I am constructing a JSON object (map) and I need to conditionally set a field. I’m trying to write proper elixir-way code… and I’m at a l...
New
WestKeys
Currently suffering from paralysis by [HTTP client] analysis. This is rather unusual in Elixirland as there tends to be consensus on the ...
New
Kurisu
For example for a current url like http://localhost:4000/cosmetic/products?_utf8=✓&query=perfume&page=2, I would like to get: ...
New
albydarned
Hello all! I am typing this post from my new MacBook Pro with the M1 chip. I’m loving it so far, and will probably use it as my daily dr...
New
alice
Hey, Just curious what are the main benefits of Elixir compared to Clojure? When is Elixir more useful than Clojure and vice versa? Th...
New
nobody
Hi! In PHP: $_SERVER[‘SERVER_ADDR’] - in Elixir? Searched the docs for ip address and the web, no good results. Thanks!
New
dblack
I’ve got an issue with an app and I’ve no idea of how to troubleshoot it. I’m hoping someone here might have seen something similar. I p...
New
openscript
Hello! Sorry for this astonishing simple question, but I’m really stuck. I try to set up the intellij-elixir plugin, but I don’t know ho...
New
hariharasudhan94
Lets say I have map like this fetching from my database %{"_id" => #BSON.ObjectId<58eb1a7a9ad169198c3dXXXX>, "email" => ...
New

Other popular topics Top

senggen
Erlang/OTP 25 [erts-13.2.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] 15:22:35.803 [error] gen_event {lager_file_backend...
New
AstonJ
Posting this to see if we can make things easier for people to get into Neovim. If you use Neovim and have a favourite distro please let ...
New
Darmani72
If I have a post route which an argument: post /my_post_route/:my_param1, MyController.my_post_handler How would get the post params ...
New
stefanluptak
Hello everybody, usually, I use a 29" ultra-wide monitor for VSCode which can easily accomodate explorer (files panel) + file with code ...
New
vrod
I am using the Starship cross-shell prompt – it seems pretty nice, but I get some errors: [WARN] - (starship::utils): Executing command ...
New
aalberti333
As the title describes, I’m trying to run Enum.map() over a list of key/value pairs, where the value is a map. My data looks like this: ...
New
saif
Hello everyone, Long time lurker first time poster here. I’ve recently begun working on Elixir full-time again! :raised_hands: It’s been...
New
KronicDeth
Elixir plugin for JetBrain’s IntelliJ Platform (including Rubymine) This is a plugin that adds support for Elixir to JetBrains IntelliJ...
289 36352 110
New
boundedvariable
I am going through the kafka architecture. All the features what the kafka is providing are already in Erlang. I would like hear your opi...
New
komlanvi
Hi everyone, I was playing with phoenix liveView but I run into an issue. I have a form and want to validate each input text when the te...
New

Latest on Elixir Forum

Elixir Forum

We're in Beta

About us Mission Statement