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
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
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 ![]()
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
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.
Popular in Questions
Other popular topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #websockets
- #supervisor
- #advent-of-code
- #elixirconf-us
- #distillery
- #processes
- #forms
- #api
- #metaprogramming
- #security
- #performance









