Authenticating an SPA via graphql

Anyone have any tips for authenticating an SPA via graphql? I've got basic ash_authentication boilerplate user/token resources setup. I'm wondering if the best way to do this is to wire up my own query/mutation that can set the token as a cookie... kinda trying to keep the elixir side more purely graphql, so my SPA is the main UI interface and I'm not using the ash_authentication_phoenix routes.
Solution:
"Authenticating via GraphQL"
Jump to solution
28 Replies
Jesse Williams
Jesse WilliamsOP•5d ago
That seems more like how to set the actor assuming the token exists on the request (if I understand correctly) but I'm looking more for how to perform the sign in via gql
ZachDaniel
ZachDaniel•5d ago
oh right, you're asking about the mutations/queries I thought we had something for that Do you have the book?
Jesse Williams
Jesse WilliamsOP•5d ago
I do!
ZachDaniel
ZachDaniel•5d ago
There is a section in the book that describes it
Solution
ZachDaniel
ZachDaniel•5d ago
"Authenticating via GraphQL"
Jesse Williams
Jesse WilliamsOP•5d ago
Oh awesome. This looks basically like what I was trying but I appear to have gotten the semantics slightly wrong haha. I was doing
queries do
read_one :sign_in_with_password, :sign_in_with_password do
type_name :user_with_token
end
end
queries do
read_one :sign_in_with_password, :sign_in_with_password do
type_name :user_with_token
end
end
using read_one instead of get with identity false. Technically the way I was doing it works but it winds up with a filter input which makes sense in terms of ash semantics but in terms of an actual sign in endpoint is bizarre so I was wondering if I was totally missing something. Seems like not, just a slight semantic difference. Thanks!
Jesse Williams
Jesse WilliamsOP•5d ago
Supposing I wanted the token to wind up in a cookie set by the server... is the only way to make my own resolver that sets the cookie on the connection?
ZachDaniel
ZachDaniel•5d ago
You can use modify_resolution IIRC which lets you modify the conn
Jesse Williams
Jesse WilliamsOP•5d ago
Hmm. The Absinthe.Resolution doesn't seem to have the conn in it at all 🤔 Based on my limited understanding of absinthe, it seems like modifying the conn and modifying the resolution are 2 separate things (see here). I'm wondering if this should be a separate piece of the ash_graphql dsl, like before_send that can mess with the conn I'd be happy to try my hand at a PR that does this (assuming I'm correct in my understanding that the current DSL doesn't actually support it) Actually I'm realizing it's not so simple ha, since modifying the conn happens at the plug level (which is detached from the ash dsl).
ZachDaniel
ZachDaniel•5d ago
I could have sworn there was a way to do it with the dsl
Jesse Williams
Jesse WilliamsOP•5d ago
Based on the docs it seems like that was the intention. but as far as I can see there's no way to modify the conn except in the absinthe plug, which the ash dsl doesn't configure at all as of now. I think it might work to use modify_resolution to set some auth_token param somewhere in the resolution and then have code in my application that uses the absinthe plug's before_send option to grab that and set a cookie. It's possible that's what the docs were trying to communicate, they do link out to the section of the absinte plug docs about before_send after all. It'll be a bit before I can get back to experimenting with this but when I do I'll probably submit a PR with some more in-depth docs for how to accomplish this for future folks that may be trying to use cookies for this.
ZachDaniel
ZachDaniel•5d ago
Okay So, I see where the disconnect is You have to couple it with something https://hexdocs.pm/absinthe_plug/Absinthe.Plug.html#module-before-send You use the before_send option on the absinthe plug Which gets the context that is set
Jesse Williams
Jesse WilliamsOP•5d ago
yeah, that's what I was thinking
ZachDaniel
ZachDaniel•5d ago
So: 1. modify_resolution to set context in resolver 2. before_send to extract and modify conn would be a great addition to the docs.
Jesse Williams
Jesse WilliamsOP•5d ago
yup, cool. I'll submit a docs PR (might be a couple days ha)
ZachDaniel
ZachDaniel•5d ago
😄 BTW: we're finding this is a great task for claude 4 FWIW
Jesse Williams
Jesse WilliamsOP•5d ago
Running into this on a personal project, gotta go do real work now 😂
ZachDaniel
ZachDaniel•5d ago
open up the ash_graphql repo, paste in this thread and say "fix the docs" to avoid this issue
Jesse Williams
Jesse WilliamsOP•5d ago
Updating docs? Ah right probably a good idea, I'm notoriously bad at writing docs 😂
ZachDaniel
ZachDaniel•5d ago
I've considered even automating it from these help threads 😆
Failz
Failz•5d ago
How about logging out? Should Token.actions.revoke_token be exposed as a gql mutation with an appropriate policy? I don't recall seeing logout mentioned in the book.
ZachDaniel
ZachDaniel•4d ago
I think that would make sense. No need for a special policy though, since it would just log out whoever the current actor is
Failz
Failz•4d ago
policies do
bypass AshAuthentication.Checks.AshAuthenticationInteraction do
description "AshAuthentication can interact with the token resource"
authorize_if always()
end

policy always() do
description "No one aside from AshAuthentication can interact with the tokens resource."
forbid_if always()
end
end
policies do
bypass AshAuthentication.Checks.AshAuthenticationInteraction do
description "AshAuthentication can interact with the token resource"
authorize_if always()
end

policy always() do
description "No one aside from AshAuthentication can interact with the tokens resource."
forbid_if always()
end
end
how does AshAuth come into play over graphql?
Jesse Williams
Jesse WilliamsOP•4d ago
^ I was adding my own policies there. e.g.
policy action([:register, :sign_in]) do
authorize_if always()
end
policy action([:register, :sign_in]) do
authorize_if always()
end
(and removed the forbid_if always() policy) Definitely seems that the default policies are kinda optimizing for the use case of ash_authentication_phoenix I can see why though, policies for interacting with users are one of those things that could depend a lot on your application use case
ZachDaniel
ZachDaniel•4d ago
Yeah, its just there as a safety measure or to make it clear that nothings allowed
Failz
Failz•4d ago
I tried exposing revoke_token to gql but didn't have much luck. I kept running into forbidden. It looks like it wanted authorized for other token related actions. I gave up but will circle back at some point. Something else to note; I was also signing in with oauth against Entra ID (Azure) and then in the callback after Entra posts back to Phoenix I was manually generating a token. I noticed that two tokens were being created. Turns out App.Accounts.user_register_with_microsoft was creating a token via change AshAuthentication.GenerateTokenChange in the register_with_microsoft action so my manual token creation was duplicative. The original token gets stuffed into metadata so I was able to just grab that and remove my manual token creation.
defp authenticate_user(user_info, tokens) do
case App.Accounts.user_register_with_microsoft(user_info, tokens) do
{:ok, user} ->
# Get the token that was already generated by AshAuthentication.GenerateTokenChange
case Ash.Resource.get_metadata(user, :token) do
nil ->
{:error, :authentication_failed, "No token was generated"}

token ->
{:ok, user, token}
end

{:error, _reason} ->
{:error, :authentication_failed, "Failed to register user"}
end
end
defp authenticate_user(user_info, tokens) do
case App.Accounts.user_register_with_microsoft(user_info, tokens) do
{:ok, user} ->
# Get the token that was already generated by AshAuthentication.GenerateTokenChange
case Ash.Resource.get_metadata(user, :token) do
nil ->
{:error, :authentication_failed, "No token was generated"}

token ->
{:ok, user, token}
end

{:error, _reason} ->
{:error, :authentication_failed, "Failed to register user"}
end
end
ZachDaniel
ZachDaniel•4d ago
If you're getting forbidden it is likely from policies on the user You'll have to set them up to allow any given user to call that action

Did you find this page helpful?