mirror of
https://github.com/team-alembic/ash_authentication.git
synced 2024-09-20 05:13:10 +12:00
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:
parent
3413ef8d6f
commit
ca3dac3878
3 changed files with 42 additions and 3 deletions
|
@ -75,7 +75,8 @@ defmodule AshAuthentication.Plug.Helpers do
|
||||||
|
|
||||||
with token when is_binary(token) <-
|
with token when is_binary(token) <-
|
||||||
Conn.get_session(conn, "#{options.subject_name}_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, [_]} <-
|
{:ok, [_]} <-
|
||||||
TokenResource.Actions.get_token(token_resource, %{
|
TokenResource.Actions.get_token(token_resource, %{
|
||||||
"jti" => jti,
|
"jti" => jti,
|
||||||
|
@ -116,7 +117,8 @@ defmodule AshAuthentication.Plug.Helpers do
|
||||||
|> Stream.filter(&String.starts_with?(&1, "Bearer "))
|
|> Stream.filter(&String.starts_with?(&1, "Bearer "))
|
||||||
|> Stream.map(&String.replace_leading(&1, "Bearer ", ""))
|
|> Stream.map(&String.replace_leading(&1, "Bearer ", ""))
|
||||||
|> Enum.reduce(conn, fn token, conn ->
|
|> 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 <- validate_token(resource, jti),
|
||||||
{:ok, user} <- AshAuthentication.subject_to_user(subject, resource),
|
{:ok, user} <- AshAuthentication.subject_to_user(subject, resource),
|
||||||
{:ok, subject_name} <- Info.authentication_subject_name(resource),
|
{:ok, subject_name} <- Info.authentication_subject_name(resource),
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
defmodule AshAuthentication.Plug.HelpersTest do
|
defmodule AshAuthentication.Plug.HelpersTest do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
use DataCase, async: true
|
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]
|
import Plug.Test, only: [conn: 3]
|
||||||
alias Plug.Conn
|
alias Plug.Conn
|
||||||
|
|
||||||
|
@ -106,6 +106,20 @@ defmodule AshAuthentication.Plug.HelpersTest do
|
||||||
|
|
||||||
refute conn.assigns.current_user_with_token_required
|
refute conn.assigns.current_user_with_token_required
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
describe "retrieve_from_bearer/2" do
|
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)
|
refute is_map_key(conn.assigns, :current_user_with_token_required)
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
describe "revoke_bearer_tokens/2" do
|
describe "revoke_bearer_tokens/2" do
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
defmodule Example.UserWithTokenRequired do
|
defmodule Example.UserWithTokenRequired do
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
use Ash.Resource, data_layer: AshPostgres.DataLayer, extensions: [AshAuthentication]
|
use Ash.Resource, data_layer: AshPostgres.DataLayer, extensions: [AshAuthentication]
|
||||||
|
require Logger
|
||||||
|
|
||||||
@type t :: %__MODULE__{
|
@type t :: %__MODULE__{
|
||||||
id: Ecto.UUID.t(),
|
id: Ecto.UUID.t(),
|
||||||
|
@ -32,6 +33,14 @@ defmodule Example.UserWithTokenRequired do
|
||||||
strategies do
|
strategies do
|
||||||
password do
|
password do
|
||||||
identity_field :email
|
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
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue