ash_authentication update totally breaks a signed in user

If i was already logged in, i get redirected to the login page, but it's perptually redirecting me with the attached log
22 Replies
sevenseacat
sevenseacat•4mo ago
the first two lines look very suspicious - HomeLive.nil? and there's no loop there, what do you mean by perpetually redirecting?
ZachDaniel
ZachDaniel•4mo ago
The load from session plug should clear the session on a failed attempt to sign in I tested this scenario as well and didn't have any issues like this. What exact change did you make? Did you remove the params from that last Ecto query log? Or is it actually sending a bunch of commas like that?
allenwyma
allenwymaOP•4mo ago
here's by browser pipeline:
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session

plug Cldr.Plug.PutLocale,
apps: [:cldr, :gettext],
gettext: CauseBeaconWeb.Gettext,
from: [:query, :session, :accept_language],
cldr: CauseBeacon.Cldr

plug Cldr.Plug.PutSession, as: :string
plug :fetch_live_flash
plug :put_root_layout, html: {CauseBeaconWeb.Layouts, :root}
plug :protect_from_forgery
plug :put_secure_browser_headers
# this set the user on the conn
plug :load_from_session, otp_app: :cause_beacon
plug CauseBeaconWeb.Plugs.LoadUser
plug CauseBeaconWeb.Plugs.SetTenant
plug CauseBeaconWeb.Plugs.SetTenantFromParams
plug CauseBeaconWeb.Plugs.SetSideFromParams
plug CauseBeaconWeb.Plugs.SetCurrentPath
plug :set_actor, :user
end
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session

plug Cldr.Plug.PutLocale,
apps: [:cldr, :gettext],
gettext: CauseBeaconWeb.Gettext,
from: [:query, :session, :accept_language],
cldr: CauseBeacon.Cldr

plug Cldr.Plug.PutSession, as: :string
plug :fetch_live_flash
plug :put_root_layout, html: {CauseBeaconWeb.Layouts, :root}
plug :protect_from_forgery
plug :put_secure_browser_headers
# this set the user on the conn
plug :load_from_session, otp_app: :cause_beacon
plug CauseBeaconWeb.Plugs.LoadUser
plug CauseBeaconWeb.Plugs.SetTenant
plug CauseBeaconWeb.Plugs.SetTenantFromParams
plug CauseBeaconWeb.Plugs.SetSideFromParams
plug CauseBeaconWeb.Plugs.SetCurrentPath
plug :set_actor, :user
end
yes, i also agree, hence i was a bit confused? If i use incognito mode, i have no issues, can login, no issues. but my existing session in google chrome is having this issue after making the changes and i'm not sure. this happened when i upgraded to the latest version of ash_authentication with the changes to using :jti and requiring. Here's my tokens section:
authentication do
tokens do
enabled? true
token_resource CauseBeacon.Accounts.Token
signing_secret CauseBeacon.Secrets
store_all_tokens? true
session_identifier :jti
require_token_presence_for_authentication? true
end
...
authentication do
tokens do
enabled? true
token_resource CauseBeacon.Accounts.Token
signing_secret CauseBeacon.Secrets
store_all_tokens? true
session_identifier :jti
require_token_presence_for_authentication? true
end
...
ZachDaniel
ZachDaniel•4mo ago
What does the browser request logs look like? Essentially, you should be getting into this code: |> Conn.assign(current_subject_name, nil)
ZachDaniel
ZachDaniel•4mo ago
irondeau
irondeau•4mo ago
I'm experiencing the same issue described above. I've found that the :live_user_required hook evaluates to false
def on_mount(:live_user_required, _params, _session, socket) do
if socket.assigns[:current_user] do # evaluates to `false`
{:cont, socket}
else
{:halt, Phoenix.LiveView.redirect(socket, to: ~p"/sign-in")}
end
end
def on_mount(:live_user_required, _params, _session, socket) do
if socket.assigns[:current_user] do # evaluates to `false`
{:cont, socket}
else
{:halt, Phoenix.LiveView.redirect(socket, to: ~p"/sign-in")}
end
end
I only added the require_token_presence_for_authentication? true line. Have not set the session_identifier
ZachDaniel
ZachDaniel•4mo ago
So, does this mean that the user session is not being cleared? This logic here:
@spec retrieve_from_session(Conn.t(), module, keyword) :: Conn.t()
def retrieve_from_session(conn, otp_app, opts \\ []) do
opts =
opts
|> Keyword.put_new(:tenant, Ash.PlugHelpers.get_tenant(conn))
|> Keyword.put_new(:context, Ash.PlugHelpers.get_context(conn) || %{})

otp_app
|> AshAuthentication.authenticated_resources()
|> Stream.map(
&{&1, Info.authentication_options(&1),
Info.authentication_tokens_require_token_presence_for_authentication?(&1)}
)
|> Enum.reduce(conn, fn
{resource, options, true}, conn ->
current_subject_name = current_subject_name(options.subject_name)
token_resource = Info.authentication_tokens_token_resource!(resource)
session_key = "#{options.subject_name}_token"

with token when is_binary(token) <-
Conn.get_session(conn, session_key),
{:ok, %{"sub" => subject, "jti" => jti} = claims, _}
when not is_map_key(claims, "act") <- Jwt.verify(token, otp_app, opts),
{:ok, [_]} <-
TokenResource.Actions.get_token(
token_resource,
%{
"jti" => jti,
"purpose" => "user"
},
opts
),
{:ok, user} <-
AshAuthentication.subject_to_user(
subject,
resource,
opts
) do
Conn.assign(conn, current_subject_name, user)
else
_ ->
conn
|> Conn.assign(current_subject_name, nil)
|> Conn.delete_session(session_key)
end

{resource, options, false}, conn ->
current_subject_name = current_subject_name(options.subject_name)

with subject when is_binary(subject) <- Conn.get_session(conn, options.subject_name),
{:ok, subject} <- split_identifier(subject, resource),
{:ok, user} <-
AshAuthentication.subject_to_user(
subject,
resource,
opts
) do
Conn.assign(conn, current_subject_name, user)
else
_ ->
conn
|> Conn.assign(current_subject_name, nil)
|> Conn.delete_session(options.subject_name)
end
end)
end
@spec retrieve_from_session(Conn.t(), module, keyword) :: Conn.t()
def retrieve_from_session(conn, otp_app, opts \\ []) do
opts =
opts
|> Keyword.put_new(:tenant, Ash.PlugHelpers.get_tenant(conn))
|> Keyword.put_new(:context, Ash.PlugHelpers.get_context(conn) || %{})

otp_app
|> AshAuthentication.authenticated_resources()
|> Stream.map(
&{&1, Info.authentication_options(&1),
Info.authentication_tokens_require_token_presence_for_authentication?(&1)}
)
|> Enum.reduce(conn, fn
{resource, options, true}, conn ->
current_subject_name = current_subject_name(options.subject_name)
token_resource = Info.authentication_tokens_token_resource!(resource)
session_key = "#{options.subject_name}_token"

with token when is_binary(token) <-
Conn.get_session(conn, session_key),
{:ok, %{"sub" => subject, "jti" => jti} = claims, _}
when not is_map_key(claims, "act") <- Jwt.verify(token, otp_app, opts),
{:ok, [_]} <-
TokenResource.Actions.get_token(
token_resource,
%{
"jti" => jti,
"purpose" => "user"
},
opts
),
{:ok, user} <-
AshAuthentication.subject_to_user(
subject,
resource,
opts
) do
Conn.assign(conn, current_subject_name, user)
else
_ ->
conn
|> Conn.assign(current_subject_name, nil)
|> Conn.delete_session(session_key)
end

{resource, options, false}, conn ->
current_subject_name = current_subject_name(options.subject_name)

with subject when is_binary(subject) <- Conn.get_session(conn, options.subject_name),
{:ok, subject} <- split_identifier(subject, resource),
{:ok, user} <-
AshAuthentication.subject_to_user(
subject,
resource,
opts
) do
Conn.assign(conn, current_subject_name, user)
else
_ ->
conn
|> Conn.assign(current_subject_name, nil)
|> Conn.delete_session(options.subject_name)
end
end)
end
Note the Conn.delete_session that should be deleting the session that holds the now invalid session key When you look at the storage, is that happening properly?
irondeau
irondeau•4mo ago
If I understand you correctly, I'm now looking at the tokens table in the database. I just cleared the rows in that table, went thru the sign-in workflow, and was redirected back to the home page. The table now has one row as follows:
"updated_at","created_at","extra_data","purpose","expires_at","subject","jti"
"2025-06-22 05:57:32.821","2025-06-22 05:57:32.821",NULL,"revocation","2025-06-22 05:58:32","member?id=118f1084-a7ed-46e1-94f6-abf5481d493b","315kfkptp1880vr9n8000892"
"updated_at","created_at","extra_data","purpose","expires_at","subject","jti"
"2025-06-22 05:57:32.821","2025-06-22 05:57:32.821",NULL,"revocation","2025-06-22 05:58:32","member?id=118f1084-a7ed-46e1-94f6-abf5481d493b","315kfkptp1880vr9n8000892"
From this block, the call to TokenResource.Actions.get_token\3 returns {:ok, []} which doesn't match the with pattern correctly all of my token purposes in this table are revocation
ZachDaniel
ZachDaniel•4mo ago
So that not matching the with pattern should be clearing your session Then when you get redirected, you should only be redirected one time
irondeau
irondeau•4mo ago
yes, the session is getting cleared, but this shouldn't be the case immediately after signing in I might be misunderstanding, but when I read the line
TokenResource.Actions.get_token(
token_resource,
%{
"jti" => jti,
"purpose" => "user"
},
opts
)
TokenResource.Actions.get_token(
token_resource,
%{
"jti" => jti,
"purpose" => "user"
},
opts
)
I expect that the row representing the session I just established thru the sign-in process should have a value of user on the purpose column. This is not the case. Instead, that column has a value revocation.
irondeau
irondeau•4mo ago
irondeau
irondeau•4mo ago
this might be interesting. the INSERT INTO query from my sign_in_with_token action inserts a JTI value of 315m73fqvfks1amh3k000201 while the SELECT query used to search for the token is searching for a JTI value of 315m73g6shcs0sotn0000354
ZachDaniel
ZachDaniel•4mo ago
The session should get cleared when the session value is invalid So when the user comes in with a session that is no longer valid it gets cleared and they should be redirected to sign in We're going to have to get this reproduced I think Please make an application using the installer and set the session identifier to :unsafe To reproduce this issue and confirm that it's an issue with ash authentication and not in some way with your implementation
irondeau
irondeau•4mo ago
I created a project from scratch and there is a row in the tokens table with a purpose of user. I can correctly log in and out. I've found the issue. In the old project, authentication.tokens.store_all_tokens? was not configured to be true
ZachDaniel
ZachDaniel•4mo ago
Hmm...that shouldn't be required, so that's interesting. So you're saying without that setting you get an infinite redirect loop?
irondeau
irondeau•4mo ago
For me, it's not an infinite redirect loop
ZachDaniel
ZachDaniel•4mo ago
Oh It's supposed to logout current users That's a side effect of the change
irondeau
irondeau•4mo ago
Maybe my issue was different than the original post There was no token stored with a user purpose until I enable that config setting What would have caused an infinite loop is if my home page required user authentication But I don't have it set up that way
ZachDaniel
ZachDaniel•4mo ago
There doesn't have to be a token with a user purpose unless that setting is enabled So whatever you experienced it doesn't sound like a bug, just expected behavior?
allenwyma
allenwymaOP•4mo ago
for the time being: we ended up just asking users to go to /sign-out to log out, then logging back in doens't have this issue. I'm not sure what happened but i was in a bottomless loop, as stated above. Sorry about the late reply. So the main issue is old sessions after adding in those two lines had the boot loop and singing out and signing back in seems to fix the issue. Removing the line and using the unsafe will also not have the boot loop.
ZachDaniel
ZachDaniel•4mo ago
😲😖 I never saw that so there is only so much I can do unfortunately
allenwyma
allenwymaOP•4mo ago
it's all good 🙂 the great thing about having not many users: you can let them be the guinea pig 😉

Did you find this page helpful?