Trouble setting up Ash Authentication with Google

I’m newer to Elixir/Phoenix in general, but decided to give Ash a go because I really like the philosophy. However, I’m running into trouble setting up Google Oauth with Ash Authentication.

Actually, I set up magic link first using Igniter, but then realized in production, I can’t send emails yet, so I added password authentication. (This seemed to break magic link :frowning_face:)

Now I’m trying to add Google, as I want to build a prototype to read emails. After following instructions, the Sign in with Google button shows up, but clicking it just returns me back to /sign-in page.

There are no errors in the console… and this is where I’m a bit stuck with the Ash philosophy. If I’m declaring everything, how do I trace the code to find out what’s wrong? Am I just guessing which attributes are missing?

Here is my User resource:

defmodule Playground.Accounts.User do use Ash.Resource, otp_app: :playground, domain: Playground.Accounts, authorizers: [Ash.Policy.Authorizer], extensions: [AshAuthentication], data_layer: AshSqlite.DataLayer authentication do add_ons do log_out_everywhere do apply_on_password_change?(true) end confirmation :confirm_new_user do monitor_fields [:email] confirm_on_create? true confirm_on_update? false auto_confirm_actions [ :register_with_google, :sign_in_with_magic_link, :reset_password_with_token ] sender Playground.Accounts.User.Senders.SendNewUserConfirmationEmail end end tokens do enabled? true token_resource Playground.Accounts.Token signing_secret Playground.Secrets store_all_tokens? true require_token_presence_for_authentication? true end strategies do magic_link do identity_field :email registration_enabled? true sender Playground.Accounts.User.Senders.SendMagicLinkEmail end password :password do identity_field :email resettable do sender Playground.Accounts.User.Senders.SendPasswordResetEmail # these configurations will be the default in a future release password_reset_action_name :reset_password_with_token request_password_reset_action_name :request_password_reset_token end end google do client_id Playground.Secrets redirect_uri Playground.Secrets client_secret Playground.Secrets end end end sqlite do table "users" repo Playground.Repo end actions do defaults [:read] read :get_by_subject do description "Get a user by the subject claim in a JWT" argument :subject, :string, allow_nil?: false get? true prepare AshAuthentication.Preparations.FilterBySubject end read :get_by_email do description "Looks up a user by their email" get? true argument :email, :ci_string do allow_nil? false end filter expr(email == ^arg(:email)) end create :sign_in_with_magic_link do description "Sign in or register a user with magic link." argument :token, :string do description "The token from the magic link that was sent to the user" allow_nil? false end upsert? true upsert_identity :unique_email upsert_fields [:email, :confirmed_at] # Uses the information from the token to create or sign in the user change AshAuthentication.Strategy.MagicLink.SignInChange metadata :token, :string do allow_nil? false end end action :request_magic_link do argument :email, :ci_string do allow_nil? false end run AshAuthentication.Strategy.MagicLink.Request end update :change_password do # Use this action to allow users to change their password by providing # their current password and a new password. require_atomic? false accept [] argument :current_password, :string, sensitive?: true, allow_nil?: false argument :password, :string, sensitive?: true, allow_nil?: false argument :password_confirmation, :string, sensitive?: true, allow_nil?: false validate confirm(:password, :password_confirmation) validate {AshAuthentication.Strategy.Password.PasswordValidation, strategy_name: :password, password_argument: :current_password} change {AshAuthentication.Strategy.Password.HashPasswordChange, strategy_name: :password} end read :sign_in_with_password do description "Attempt to sign in using a email and password." get? true argument :email, :ci_string do description "The email to use for retrieving the user." allow_nil? false end argument :password, :string do description "The password to check for the matching user." allow_nil? false sensitive? true end # validates the provided email and password and generates a token prepare AshAuthentication.Strategy.Password.SignInPreparation metadata :token, :string do description "A JWT that can be used to authenticate the user." allow_nil? false end end read :sign_in_with_token do # In the generated sign in components, we validate the # email and password directly in the LiveView # and generate a short-lived token that can be used to sign in over # a standard controller action, exchanging it for a standard token. # This action performs that exchange. If you do not use the generated # liveviews, you may remove this action, and set # `sign_in_tokens_enabled? false` in the password strategy. description "Attempt to sign in using a short-lived sign in token." get? true argument :token, :string do description "The short-lived sign in token." allow_nil? false sensitive? true end # validates the provided sign in token and generates a token prepare AshAuthentication.Strategy.Password.SignInWithTokenPreparation metadata :token, :string do description "A JWT that can be used to authenticate the user." allow_nil? false end end create :register_with_password do description "Register a new user with a email and password." argument :email, :ci_string do allow_nil? false end argument :password, :string do description "The proposed password for the user, in plain text." allow_nil? false constraints min_length: 8 sensitive? true end argument :password_confirmation, :string do description "The proposed password for the user (again), in plain text." allow_nil? false sensitive? true end # Sets the email from the argument change set_attribute(:email, arg(:email)) # Hashes the provided password change AshAuthentication.Strategy.Password.HashPasswordChange # Generates an authentication token for the user change AshAuthentication.GenerateTokenChange # validates that the password matches the confirmation validate AshAuthentication.Strategy.Password.PasswordConfirmationValidation metadata :token, :string do description "A JWT that can be used to authenticate the user." allow_nil? false end end action :request_password_reset_token do description "Send password reset instructions to a user if they exist." argument :email, :ci_string do allow_nil? false end # creates a reset token and invokes the relevant senders run {AshAuthentication.Strategy.Password.RequestPasswordReset, action: :get_by_email} end update :reset_password_with_token do argument :reset_token, :string do allow_nil? false sensitive? true end argument :password, :string do description "The proposed password for the user, in plain text." allow_nil? false constraints min_length: 8 sensitive? true end argument :password_confirmation, :string do description "The proposed password for the user (again), in plain text." allow_nil? false sensitive? true end # validates the provided reset token validate AshAuthentication.Strategy.Password.ResetTokenValidation # validates that the password matches the confirmation validate AshAuthentication.Strategy.Password.PasswordConfirmationValidation # Hashes the provided password change AshAuthentication.Strategy.Password.HashPasswordChange # Generates an authentication token for the user change AshAuthentication.GenerateTokenChange end create :register_with_google do argument :user_info, :map, allow_nil?: false argument :oauth_tokens, :map, allow_nil?: false upsert? true upsert_identity :unique_email change AshAuthentication.GenerateTokenChange # Required if you have the `identity_resource` configuration enabled. change AshAuthentication.Strategy.OAuth2.IdentityChange change fn changeset, _ -> user_info = Ash.Changeset.get_argument(changeset, :user_info) Ash.Changeset.change_attributes(changeset, Map.take(user_info, ["email"])) end # Required if you're using the password & confirmation strategies upsert_fields [] change set_attribute(:confirmed_at, &DateTime.utc_now/0) change after_action(fn _changeset, user, _context -> case user.confirmed_at do nil -> {:error, "Unconfirmed user exists already"} _ -> {:ok, user} end end) end end policies do bypass AshAuthentication.Checks.AshAuthenticationInteraction do authorize_if always() end policy always() do forbid_if always() end end attributes do uuid_primary_key :id attribute :email, :ci_string do allow_nil? false public? true end attribute :hashed_password, :string do allow_nil? true sensitive? true end end identities do identity :unique_email, [:email] end end 

Much appreciate any insights.

The first step would be looking at your Phoenix server logs - what’s happening when you click sign in? Is it redirecting somewhere and then redirecting back to your app?

The second part would probably be to turn on debugging for authentication failures in development - see AshAuthentication.Debug — ash_authentication v4.5.2 for details.

As to magic link being broken, we’d probably need some more information about how exactly it’s broken before we can start to diagnose how to fix it!

Ah, the debug would be very helpful! Thank you for that.

There’s a lot of documentation for Ash and it can be a bit overwhelming, but I’m getting there. :slight_smile:

Looks like the Magic Link issue is only when trying to log in to accounts that aren’t confirmed. Getting the following error:

[error] ** (Ash.Error.Unknown) Bread Crumbs: > Exception raised in: Playground.Accounts.User.sign_in_with_magic_link Unknown Error * ** (CaseClauseError) no case clause matching: {:ok, []} (ash_sqlite 0.2.3) lib/data_layer.ex:1213: AshSqlite.DataLayer.upsert/3 (ash 3.4.64) lib/ash/actions/create/create.ex:377: anonymous fn/6 in Ash.Actions.Create.commit/3 (ash 3.4.64) lib/ash/changeset/changeset.ex:4021: Ash.Changeset.run_around_actions/2 (ash 3.4.64) lib/ash/changeset/changeset.ex:3699: anonymous fn/2 in Ash.Changeset.transaction_hooks/2 (ash 3.4.64) lib/ash/changeset/changeset.ex:3609: Ash.Changeset.with_hooks/3 (ash 3.4.64) lib/ash/actions/create/create.ex:260: Ash.Actions.Create.commit/3 (ash 3.4.64) lib/ash/actions/create/create.ex:132: Ash.Actions.Create.do_run/4 (ash 3.4.64) lib/ash/actions/create/create.ex:50: Ash.Actions.Create.run/4 (ash_authentication 4.5.1) lib/ash_authentication/strategies/magic_link/actions.ex:62: AshAuthentication.Strategy.MagicLink.Actions.sign_in/3 (ash_authentication 4.5.1) lib/ash_authentication/strategies/magic_link/plug.ex:44: AshAuthentication.Strategy.MagicLink.Plug.sign_in/2 (ash_authentication 4.5.1) lib/ash_authentication/plug/dispatcher.ex:29: AshAuthentication.Plug.Dispatcher.call/2 (phoenix 1.7.18) lib/phoenix/router/route.ex:42: Phoenix.Router.Route.call/2 (phoenix 1.7.18) lib/phoenix/router.ex:484: Phoenix.Router.__call__/5 (playground 0.1.0) lib/playground_web/endpoint.ex:1: PlaygroundWeb.Endpoint.plug_builder_call/2 (playground 0.1.0) deps/plug/lib/plug/debugger.ex:136: PlaygroundWeb.Endpoint."call (overridable 3)"/2 (playground 0.1.0) lib/playground_web/endpoint.ex:1: PlaygroundWeb.Endpoint.call/2 (phoenix 1.7.18) lib/phoenix/endpoint/sync_code_reload_plug.ex:22: Phoenix.Endpoint.SyncCodeReloadPlug.do_call/4 (bandit 1.6.6) lib/bandit/pipeline.ex:129: Bandit.Pipeline.call_plug!/2 (bandit 1.6.6) lib/bandit/pipeline.ex:40: Bandit.Pipeline.run/4 (bandit 1.6.6) lib/bandit/http1/handler.ex:12: Bandit.HTTP1.Handler.handle_data/3 (ash_sqlite 0.2.3) lib/data_layer.ex:1213: AshSqlite.DataLayer.upsert/3 (ash 3.4.64) lib/ash/actions/create/create.ex:377: anonymous fn/6 in Ash.Actions.Create.commit/3 (ash 3.4.64) lib/ash/changeset/changeset.ex:4021: Ash.Changeset.run_around_actions/2 (ash 3.4.64) lib/ash/changeset/changeset.ex:3699: anonymous fn/2 in Ash.Changeset.transaction_hooks/2 (ash 3.4.64) lib/ash/changeset/changeset.ex:3609: Ash.Changeset.with_hooks/3 (ash 3.4.64) lib/ash/actions/create/create.ex:260: Ash.Actions.Create.commit/3 (ash 3.4.64) lib/ash/actions/create/create.ex:132: Ash.Actions.Create.do_run/4 (ash 3.4.64) lib/ash/actions/create/create.ex:50: Ash.Actions.Create.run/4 (ash_authentication 4.5.1) lib/ash_authentication/strategies/magic_link/actions.ex:62: AshAuthentication.Strategy.MagicLink.Actions.sign_in/3 (ash_authentication 4.5.1) lib/ash_authentication/strategies/magic_link/plug.ex:44: AshAuthentication.Strategy.MagicLink.Plug.sign_in/2 (ash_authentication 4.5.1) lib/ash_authentication/plug/dispatcher.ex:29: AshAuthentication.Plug.Dispatcher.call/2 (phoenix 1.7.18) lib/phoenix/router/route.ex:42: Phoenix.Router.Route.call/2 (phoenix 1.7.18) lib/phoenix/router.ex:484: Phoenix.Router.__call__/5 (playground 0.1.0) lib/playground_web/endpoint.ex:1: PlaygroundWeb.Endpoint.plug_builder_call/2 (playground 0.1.0) deps/plug/lib/plug/debugger.ex:136: PlaygroundWeb.Endpoint."call (overridable 3)"/2 (playground 0.1.0) lib/playground_web/endpoint.ex:1: PlaygroundWeb.Endpoint.call/2 (phoenix 1.7.18) lib/phoenix/endpoint/sync_code_reload_plug.ex:22: Phoenix.Endpoint.SyncCodeReloadPlug.do_call/4 (bandit 1.6.6) lib/bandit/pipeline.ex:129: Bandit.Pipeline.call_plug!/2 (bandit 1.6.6) lib/bandit/pipeline.ex:40: Bandit.Pipeline.run/4 (bandit 1.6.6) lib/bandit/http1/handler.ex:12: Bandit.HTTP1.Handler.handle_data/3 

I found the Confirmation Tutorial. I’m guessing this is the issue, although the error is a bit cryptic.

Will try one of the ways to handle magic link confirmation after lunch!

This definitely looks like a bug. Please open a bug showing that stack trace on ash_sqlite

Done!

Alright, going back to debugging Google Signin…

Turned on debugging for authentication failures, but no errors.
Looks like it just hits the /auth/user/google route and redirects back to the sign in page.

[info] GET /auth/user/google [debug] Processing with AshAuthentication.Phoenix.StrategyRouter Parameters: %{} Pipelines: [:browser] [info] Sent 302 in 286µs [info] GET /sign-in [debug] Processing with AshAuthentication.Phoenix.SignInLive.sign_in/2 Parameters: %{} Pipelines: [:browser] [info] Sent 200 in 5ms [info] CONNECTED TO Phoenix.LiveView.Socket in 16µs Transport: :websocket Serializer: Phoenix.Socket.V2.JSONSerializer Parameters: %{"_csrf_token" => "QVwzNQcrHnQoBhkoGCFHcVYZcEhTCHMH6qzDkrX6aBNb5Q34oX4qf8Co", "_live_referer" => "undefined", "_mount_attempts" => "0", "_mounts" => "0", "_track_static" => %{"0" => "http://localhost:4000/assets/app.css", "1" => "http://localhost:4000/assets/app.js"}, "vsn" => "2.0.0"} [debug] MOUNT AshAuthentication.Phoenix.SignInLive Parameters: %{} Session: %{"_csrf_token" => "w-IqlYFBIDWJ-ptE9AD9500h", "auth_routes_prefix" => "/auth", "context" => nil, "gettext_fn" => nil, "otp_app" => nil, "overrides" => [PlaygroundWeb.AuthOverrides, AshAuthentication.Phoenix.Overrides.Default], "path" => "/sign-in", "register_path" => "/register", "reset_path" => "/reset", "tenant" => nil} [debug] Replied in 179µs [debug] HANDLE PARAMS in AshAuthentication.Phoenix.SignInLive Parameters: %{} [debug] Replied in 37µs 

Not sure if this is how it’s supposed to show up, but the button is grey compared to the other signin buttons.

Alright, so that first issue you mentioned should be fixed, will be released next week.

Could I see how your router is set up? Are you getting into your AuthController at all? i.e in the failure callback?

You guys are amazing! Thank you.

My router.ex:

defmodule PlaygroundWeb.Router do use PlaygroundWeb, :router use AshAuthentication.Phoenix.Router pipeline :browser do plug :accepts, ["html"] plug :fetch_session plug :fetch_live_flash plug :put_root_layout, html: {PlaygroundWeb.Layouts, :root} plug :protect_from_forgery plug :put_secure_browser_headers plug :load_from_session end pipeline :api do plug :accepts, ["json"] plug :load_from_bearer end scope "/", PlaygroundWeb do pipe_through :browser get "/", PageController, :home ash_authentication_live_session :authenticated_routes, on_mount: {PlaygroundWeb.LiveUserAuth, :live_user_required} do # in each liveview, add one of the following at the top of the module: # # If an authenticated user must be present: # on_mount {PlaygroundWeb.LiveUserAuth, :live_user_required} # # If an authenticated user *may* be present: # on_mount {PlaygroundWeb.LiveUserAuth, :live_user_optional} # # If an authenticated user must *not* be present: # on_mount {PlaygroundWeb.LiveUserAuth, :live_no_user} live "/tts", TTSLive live "/scraper", ScraperLive live "/domain", DynamicDomainLive live "/chat", ChatLive live "/voice", VoiceLive end end scope "/", PlaygroundWeb do pipe_through :browser auth_routes AuthController, Playground.Accounts.User, path: "/auth" sign_out_route AuthController # Remove these if you'd like to use your own authentication views sign_in_route register_path: "/register", reset_path: "/reset", auth_routes_prefix: "/auth", on_mount: [{PlaygroundWeb.LiveUserAuth, :live_no_user}], overrides: [ PlaygroundWeb.AuthOverrides, AshAuthentication.Phoenix.Overrides.Default ] # Remove this if you do not want to use the reset password feature reset_route auth_routes_prefix: "/auth", overrides: [ PlaygroundWeb.AuthOverrides, AshAuthentication.Phoenix.Overrides.Default ] ash_authentication_live_session :user_account_routes, on_mount: {PlaygroundWeb.LiveUserAuth, :live_user_required} do live "/account", AccountLive end end # Other scopes may use custom stacks. # scope "/api", PlaygroundWeb do # pipe_through :api # end # Enable LiveDashboard and Swoosh mailbox preview in development if Application.compile_env(:playground, :dev_routes) do # If you want to use the LiveDashboard in production, you should put # it behind authentication and allow only admins to access it. # If your application does not have an admins-only section yet, # you can use Plug.BasicAuth to set up some basic authentication # as long as you are also using SSL (which you should anyway). import Phoenix.LiveDashboard.Router scope "/dev" do pipe_through :browser live_dashboard "/dashboard", metrics: PlaygroundWeb.Telemetry forward "/mailbox", Plug.Swoosh.MailboxPreview end end end 

Here’s the log:

[info] GET /auth/user/google [debug] Processing with AshAuthentication.Phoenix.StrategyRouter Parameters: %{} Pipelines: [:browser] [info] Sent 302 in 315µs [info] GET /sign-in [debug] Processing with AshAuthentication.Phoenix.SignInLive.sign_in/2 Parameters: %{} Pipelines: [:browser] [info] Sent 200 in 5ms [info] CONNECTED TO Phoenix.LiveView.Socket in 16µs Transport: :websocket Serializer: Phoenix.Socket.V2.JSONSerializer Parameters: %{"_csrf_token" => "dBYwBBd4P2tyCRMkPGZbBgQ-HQc0GyN77AoltME8KAalN-vKjgSTulGM", "_live_referer" => "undefined", "_mount_attempts" => "0", "_mounts" => "0", "_track_static" => %{"0" => "http://localhost:4000/assets/app.css", "1" => "http://localhost:4000/assets/app.js"}, "vsn" => "2.0.0"} [debug] MOUNT AshAuthentication.Phoenix.SignInLive Parameters: %{} Session: %{"_csrf_token" => "CW_hc5zS9HrHrK-MnYNSAwd6", "auth_routes_prefix" => "/auth", "context" => nil, "gettext_fn" => nil, "otp_app" => nil, "overrides" => [PlaygroundWeb.AuthOverrides, AshAuthentication.Phoenix.Overrides.Default], "path" => "/sign-in", "register_path" => "/register", "reset_path" => "/reset", "tenant" => nil} [debug] Replied in 226µs [debug] HANDLE PARAMS in AshAuthentication.Phoenix.SignInLive Parameters: %{} [debug] Replied in 45µs 

I’m… not sure if it goes into AuthController. It looks like it hits the /auth/user/google route and just redirects.

Here’s my mix phx.routes:

 GET / PlaygroundWeb.PageController :home GET /tts PlaygroundWeb.TTSLive nil GET /scraper PlaygroundWeb.ScraperLive nil GET /domain PlaygroundWeb.DynamicDomainLive nil GET /chat PlaygroundWeb.ChatLive nil GET /voice PlaygroundWeb.VoiceLive nil * /auth AshAuthentication.Phoenix.StrategyRouter [path: "/auth", as: :auth, controller: PlaygroundWeb.AuthController, not_found_plug: nil, resources: [Playground.Accounts.User]] GET /sign-out PlaygroundWeb.AuthController :sign_out GET /sign-in AshAuthentication.Phoenix.SignInLive :sign_in GET /reset AshAuthentication.Phoenix.SignInLive :reset GET /register AshAuthentication.Phoenix.SignInLive :register GET /password-reset/:token AshAuthentication.Phoenix.ResetLive :reset GET /account PlaygroundWeb.AccountLive nil GET /dev/dashboard/css-:md5 Phoenix.LiveDashboard.Assets :css GET /dev/dashboard/js-:md5 Phoenix.LiveDashboard.Assets :js GET /dev/dashboard Phoenix.LiveDashboard.PageLive :home GET /dev/dashboard/:page Phoenix.LiveDashboard.PageLive :page GET /dev/dashboard/:node/:page Phoenix.LiveDashboard.PageLive :page * /dev/mailbox Plug.Swoosh.MailboxPreview [] WS /live/websocket Phoenix.LiveView.Socket GET /live/longpoll Phoenix.LiveView.Socket POST /live/longpoll Phoenix.LiveView.Socket GET /auth/user/confirm_new_user AshAuthentication.AddOn.Confirmation GET /auth/user/google AshAuthentication.Strategy.OAuth2 POST /auth/user/google/callback AshAuthentication.Strategy.OAuth2 GET /auth/user/password/sign_in_with_token AshAuthentication.Strategy.Password POST /auth/user/password/register AshAuthentication.Strategy.Password POST /auth/user/password/sign_in AshAuthentication.Strategy.Password POST /auth/user/password/reset_request AshAuthentication.Strategy.Password POST /auth/user/password/reset AshAuthentication.Strategy.Password POST /auth/user/magic_link/request AshAuthentication.Strategy.MagicLink GET /auth/user/magic_link AshAuthentication.Strategy.MagicLink 

The first thing to do would be to drop some dbg or IO.inspect calls in the AuthController success and failure callbacks to see if its getting there, and if its getting into the failure callback to see what the error is :smiley:

Will do. Thanks for being patient with a new Elixir developer. :slight_smile:

Of course! Welcome to Elixir :heart: We’ll get you sorted :people_hugging:

Well, after struggling a bit, looks like Claude figured it out for me. :laughing:

I added IO.inspect statements to AuthController failure callback and it said the secrets weren’t set.

Turns out I was using Application.fetch_env! with a ! and it wasn’t returning the secret in a {:ok, <secret>} format.

1 Like