fix: don't allow special purpose tokens to be used for sign in. (#191)

This fixes a security issue where someone in possession of a special purpose token (reset, confirmation, magic link, etc) would be able to access an API using this token.  We strongly encourage you to upgrade.

Closes #190.
This commit is contained in:
James Harton 2023-02-12 21:14:16 +13:00 committed by GitHub
parent 3413ef8d6f
commit ca3dac3878
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 42 additions and 3 deletions

View file

@ -75,7 +75,8 @@ defmodule AshAuthentication.Plug.Helpers do
with token when is_binary(token) <-
Conn.get_session(conn, "#{options.subject_name}_token"),
{:ok, %{"sub" => subject, "jti" => jti}, _} <- Jwt.verify(token, otp_app),
{:ok, %{"sub" => subject, "jti" => jti} = claims, _}
when not is_map_key(claims, "act") <- Jwt.verify(token, otp_app),
{:ok, [_]} <-
TokenResource.Actions.get_token(token_resource, %{
"jti" => jti,
@ -116,7 +117,8 @@ defmodule AshAuthentication.Plug.Helpers do
|> Stream.filter(&String.starts_with?(&1, "Bearer "))
|> Stream.map(&String.replace_leading(&1, "Bearer ", ""))
|> Enum.reduce(conn, fn token, conn ->
with {:ok, %{"sub" => subject, "jti" => jti}, resource} <- Jwt.verify(token, otp_app),
with {:ok, %{"sub" => subject, "jti" => jti} = claims, resource}
when not is_map_key(claims, "act") <- Jwt.verify(token, otp_app),
:ok <- validate_token(resource, jti),
{:ok, user} <- AshAuthentication.subject_to_user(subject, resource),
{:ok, subject_name} <- Info.authentication_subject_name(resource),

View file

@ -1,7 +1,7 @@
defmodule AshAuthentication.Plug.HelpersTest do
@moduledoc false
use DataCase, async: true
alias AshAuthentication.{Jwt, Plug.Helpers, TokenResource}
alias AshAuthentication.{Info, Jwt, Plug.Helpers, Strategy.Password, TokenResource}
import Plug.Test, only: [conn: 3]
alias Plug.Conn
@ -106,6 +106,20 @@ defmodule AshAuthentication.Plug.HelpersTest do
refute conn.assigns.current_user_with_token_required
end
test "when the token is for another purpose it can't be used for sign in", %{conn: conn} do
user = build_user_with_token_required()
strategy = Info.strategy!(user.__struct__, :password)
{:ok, reset_token} = Password.reset_token_for(strategy, user)
conn =
conn
|> Conn.put_session("user_with_token_required_token", reset_token)
|> Helpers.retrieve_from_session(:ash_authentication)
refute conn.assigns.current_user_with_token_required
end
end
describe "retrieve_from_bearer/2" do
@ -163,6 +177,20 @@ defmodule AshAuthentication.Plug.HelpersTest do
refute is_map_key(conn.assigns, :current_user_with_token_required)
end
test "when the token is for another purpose, it doesn't let them sign in", %{conn: conn} do
user = build_user()
strategy = Info.strategy!(user.__struct__, :password)
{:ok, reset_token} = Password.reset_token_for(strategy, user)
conn =
conn
|> Conn.put_req_header("authorization", "Bearer #{reset_token}")
|> Helpers.retrieve_from_bearer(:ash_authentication)
refute is_map_key(conn.assigns, :current_user_with_token_required)
end
end
describe "revoke_bearer_tokens/2" do

View file

@ -1,6 +1,7 @@
defmodule Example.UserWithTokenRequired do
@moduledoc false
use Ash.Resource, data_layer: AshPostgres.DataLayer, extensions: [AshAuthentication]
require Logger
@type t :: %__MODULE__{
id: Ecto.UUID.t(),
@ -32,6 +33,14 @@ defmodule Example.UserWithTokenRequired do
strategies do
password do
identity_field :email
resettable do
sender fn user, token, _opts ->
Logger.debug(
"Password reset request for user #{user.username}, token #{inspect(token)}"
)
end
end
end
end
end