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?

1 Like

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.

7 Likes

That assumes a token and a refresh token go through the same level of scrutiny, which might or might not be true. E.g. the access token might grant access statelessly (nothing on the server is checked besides the token itself), while a refresh token might look into the db for changes/invalidations/blocks/….

The access token might be used against a whole fleet of app servers, but the refresh endpoint goes back to the authentication server.

4 Likes

Also worth pointing out that, while refresh tokens can generate access tokens at will, they’re only issued in the beginning when the user authenticates. That gives an attacker a much shorter time window to steal the refresh token.

You still need to store it somewhere. In most cases it is stored in Local Storage, which is just one XSS away from being stolen.

1 Like

You need to store any token you want to use. And with XSS all bets are off anyway aren’t they?
It’s still true that if your attacker is trying to steal your token the time window to do so for refresh tokens is reduced (assuming the attacker is not inside your browser already).

You can store session in the HTTP-only cookie which is then it is not readable from the JS, which mean that it cannot be stolen by the mere XSS.

2 Likes

I agree, and this is raising the bar for attackers. But so is using refresh tokens and that’s all I was trying to say :slight_smile:

Using refresh tokens make the attack more prevalent and harder to detect if anything. Additionally approach of token + refresh token makes everything more convoluted at the same time making you less secure. Ignoring even the fact that if you want to have any authorisation, then you will need to hit DB anyway in each request that does anything sensible. So in short you made your system:

  • more error prone
  • less secure
  • more convoluted

And you have gained absolutely nothing in the process.

:+1:

2 Likes

I haven’t been keeping up with security, to the detriment of part of my career prospects I suspect.

What’s a good way to secure API access these days? (I guess the same question can be asked for e.g. keeping game sessions alive as well.)

Not with JWT apparently :laughing:

It’s a good start, I always hated working with it and it always felt like a half-solution, 99% because of the reasons you and @hauleth already enumerated above. It mostly just added complexity and extra code for not much added security (if any at all).

So what should I use if I want to have authentication/authorization for my API? What I’m really looking for is a step-by-step tutorial that I can follow, as I’m still learning Elixir and Phoenix, and these types of guides work really well for me.

1 Like

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:

8 Likes

There isn’t any use case, I just want to learn how to create Elixir/Phoenix APIs as my only current experience is Express.js with a bit of basic JWT. The idea I like the most is for it to be an API available to the public, meaning that anyone should be able to create a front-end for it.

The refresh token flow to exchange a refresh token for a new access token normally requires client credentials in the Authorization header, just like the second step of the authorization code flow where the authorization code is exchanged for an access token.

1 Like

I was joking :slight_smile:

Not an expert, but:

I have nothing against JWT a priori, I don’t think it’s “dumb” to use them. I think their advantage is simplicity. They’re simpler than creating, storing and managing tokens in your DB: you just sign the token, send it to the client and you’re done. But you pay of course for it in weakened security (number one problem IMO: can’t revoke them).

If you’re building an API for HTTP-only clients then I guess there’s nothing against using an HTTP-only cookies for API authentication, it’s certainly better protection against XSS, as explained by @hauleth. But it’s not 100% protection: if I mount a successful XSS attack against your site I can’t read your HTTP-only cookie, but I can send requests to your site and your browser will send the cookie along, thus allowing me to impersonate you and do all sorts of bad things. Successful XSS attacks are pretty much a “game over” scenario IMO.

If you just want to learn how to build authentication for Phoenix APIs then you might check out tutorials like this one. Seems pretty simple. It uses Guardian (which uses JWT), but if it’s just for learning how to build a simple API, I don’t see any issue with it. You can extend your API to use more bullet-proof security later.

Thanks for that extra tutorial. Do you perhaps have any pointers on the bullet-proof, or at least more secure ways of authenticating? Phoenix.Token gets mentioned, as well as oauth, but I’m completely at a loss because it seems like there’s a dozen different ways to go about it.

But that’s a complete deal breaker! To me every security has to start with “the server can pull your plug at any moment”. That’s how you do security in depth: you accept the possibility that a password or a token can be stolen but you also make sure they can be made null and void at the press of a button.

I agree but there’s nothing we can do as programmers. Those holes are for browser vendors to fix. :person_shrugging: What we can do is follow best practices and keep up with modern developments.

Good discussion!

Ok, but then you must know that a lot of security defaults are below your expectations. A default Phoenix app stores its session in the cookie. You can’t “pull the plug” on that from the server side, if I have that cookie I can use it until it expires. Same is true for phoenix tokens.

If you want to pull the plug from the server whenever you want against a malicious client, then you must store cookies or tokens in your backend and check them with every request. Too much overhead? That’s where refresh tokens (which have been hastily shot down above) might help: you can store and verify only the refresh token and keep the access token stateless and short-lived. In this way you can “pull the plug” on the refresh token and as soon as the access token expires and cannot be refreshed, the user is logged out.

It’s all about trade-offs really IMO, complexity/overhead vs security.

phoenix tokens may be cryptographically more secure than JWT, but you still can’t revoke them. It really depends on what kind of security you want for your API. As I said, if you’re just learning how to write an API my suggestion is to pick something simple.

2 Likes