AE
Ash Elixirβ€’3y ago
minib

GitHub authentication doesn't work

I've originally asked about this in #authentication-archive but then it was archived so I'm moving the discussion here (as it still unresolved). --------------------- Hey guys, I'm trying to setup github authentication. I followed the guide but it doesn't work 😦 when I click " Sign in with Github " it opens http://localhost:4000/auth/user/github but doesn't redirect.
I guess I should change something in my router?
# lib/open_status_web/router.ex

defmodule OpenStatusWeb.Router do
use OpenStatusWeb, :router
use AshAuthentication.Phoenix.Router

pipeline :browser do
plug(:accepts, ["html"])
plug(:fetch_session)
plug(:fetch_live_flash)
plug(:put_root_layout, {OpenStatusWeb.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 "/", OpenStatusWeb do
pipe_through(:browser)

get("/", PageController, :home)

live("/test", TestLive)

sign_in_route()
sign_out_route(AuthController)
auth_routes_for(OpenStatus.Accounts.User, to: AuthController)
end
end
# lib/open_status_web/router.ex

defmodule OpenStatusWeb.Router do
use OpenStatusWeb, :router
use AshAuthentication.Phoenix.Router

pipeline :browser do
plug(:accepts, ["html"])
plug(:fetch_session)
plug(:fetch_live_flash)
plug(:put_root_layout, {OpenStatusWeb.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 "/", OpenStatusWeb do
pipe_through(:browser)

get("/", PageController, :home)

live("/test", TestLive)

sign_in_route()
sign_out_route(AuthController)
auth_routes_for(OpenStatus.Accounts.User, to: AuthController)
end
end
# lib/open_status/secrets.ex

defmodule OpenStatus.Secrets do
use AshAuthentication.Secret

alias OpenStatus.Accounts.User

def secret_for([:authentication, :strategies, :github, :client_id], User, _) do
System.get_env("GITHUB_CLIENT_ID")
end

def secret_for([:authentication, :strategies, :github, :redirect_uri], User, _) do
System.get_env("GITHUB_REDIRECT_URI")
end

def secret_for([:authentication, :strategies, :github, :client_secret], User, _) do
System.get_env("GITHUB_CLIENT_SECRET")
end
end
# lib/open_status/secrets.ex

defmodule OpenStatus.Secrets do
use AshAuthentication.Secret

alias OpenStatus.Accounts.User

def secret_for([:authentication, :strategies, :github, :client_id], User, _) do
System.get_env("GITHUB_CLIENT_ID")
end

def secret_for([:authentication, :strategies, :github, :redirect_uri], User, _) do
System.get_env("GITHUB_REDIRECT_URI")
end

def secret_for([:authentication, :strategies, :github, :client_secret], User, _) do
System.get_env("GITHUB_CLIENT_SECRET")
end
end
GITHUB_REDIRECT_URI is set to http://localhost:4000/auth
No description
No description
No description
27 Replies
minib
minibOPβ€’3y ago
Routes
➜ open_status git:(master) βœ— mix phx.routes
page_path GET / OpenStatusWeb.PageController :home
live_path GET /test OpenStatusWeb.TestLive OpenStatusWeb.TestLive
auth_path GET /sign-in AshAuthentication.Phoenix.SignInLive :sign_in
auth_path GET /sign-out OpenStatusWeb.AuthController :sign_out
auth_path * /auth/user/password/register OpenStatusWeb.AuthController {:user, :password, :register}
auth_path * /auth/user/password/sign_in OpenStatusWeb.AuthController {:user, :password, :sign_in}
auth_path * /auth/user/github OpenStatusWeb.AuthController {:user, :github, :request}
auth_path * /auth/user/github/callback OpenStatusWeb.AuthController {:user, :github, :callback}
➜ open_status git:(master) βœ— mix phx.routes
page_path GET / OpenStatusWeb.PageController :home
live_path GET /test OpenStatusWeb.TestLive OpenStatusWeb.TestLive
auth_path GET /sign-in AshAuthentication.Phoenix.SignInLive :sign_in
auth_path GET /sign-out OpenStatusWeb.AuthController :sign_out
auth_path * /auth/user/password/register OpenStatusWeb.AuthController {:user, :password, :register}
auth_path * /auth/user/password/sign_in OpenStatusWeb.AuthController {:user, :password, :sign_in}
auth_path * /auth/user/github OpenStatusWeb.AuthController {:user, :github, :request}
auth_path * /auth/user/github/callback OpenStatusWeb.AuthController {:user, :github, :callback}
/auth/user/github responds with 401 @jart:
The 401 means that it’s not finding the route and just dropping through to the failure route. Can you show your auth dsl config?
# lib/open_status/accounts/resources/user.ex

defmodule OpenStatus.Accounts.User do
use Ash.Resource,
data_layer: AshPostgres.DataLayer,
extensions: [AshAuthentication]

attributes do
uuid_primary_key :id
attribute :name, :string
attribute :email, :ci_string, allow_nil?: false
attribute :hashed_password, :string, allow_nil?: false, sensitive?: true
end

authentication do
api OpenStatus.Accounts

strategies do
password :password do
identity_field(:email)
end

github do
client_id OpenStatus.Secrets
redirect_uri OpenStatus.Secrets
client_secret OpenStatus.Secrets
end
end

tokens do
enabled?(true)
token_resource(OpenStatus.Accounts.Token)

signing_secret(fn _, _ ->
System.fetch_env("JWT_SIGNING_SECRET")
end)
end
end

actions do
create :register_with_github do
argument :user_info, :map, allow_nil?: false
argument :oauth_tokens, :map, allow_nil?: false
upsert? true
upsert_identity :email

# Required if you have token generation enabled.
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
end
end

identities do
identity :unique_email, [:email]
end

postgres do
table "users"
repo OpenStatus.Repo
end
end
# lib/open_status/accounts/resources/user.ex

defmodule OpenStatus.Accounts.User do
use Ash.Resource,
data_layer: AshPostgres.DataLayer,
extensions: [AshAuthentication]

attributes do
uuid_primary_key :id
attribute :name, :string
attribute :email, :ci_string, allow_nil?: false
attribute :hashed_password, :string, allow_nil?: false, sensitive?: true
end

authentication do
api OpenStatus.Accounts

strategies do
password :password do
identity_field(:email)
end

github do
client_id OpenStatus.Secrets
redirect_uri OpenStatus.Secrets
client_secret OpenStatus.Secrets
end
end

tokens do
enabled?(true)
token_resource(OpenStatus.Accounts.Token)

signing_secret(fn _, _ ->
System.fetch_env("JWT_SIGNING_SECRET")
end)
end
end

actions do
create :register_with_github do
argument :user_info, :map, allow_nil?: false
argument :oauth_tokens, :map, allow_nil?: false
upsert? true
upsert_identity :email

# Required if you have token generation enabled.
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
end
end

identities do
identity :unique_email, [:email]
end

postgres do
table "users"
repo OpenStatus.Repo
end
end
ZachDaniel
ZachDanielβ€’3y ago
πŸ€” can you go to your AuthController and see if its getting to the handle_failure? There could be something totally unrelated that is broken here but the handle_failure might be swallowing any errors running the actions
minib
minibOPβ€’3y ago
defmodule OpenStatusWeb.AuthController do
use OpenStatusWeb, :controller
use AshAuthentication.Phoenix.Controller

def success(conn, _activity, user, _token) do
return_to = get_session(conn, :return_to) || ~p"/"

conn
|> delete_session(:return_to)
|> store_in_session(user)
|> assign(:current_user, user)
|> redirect(to: return_to)
end

def failure(conn, _activity, reason) do
conn
|> assign(:failure_reason, reason)
|> put_status(401)
|> render("failure.html")
end

def sign_out(conn, _params) do
return_to = get_session(conn, :return_to) || ~p"/"

conn
|> clear_session()
|> redirect(to: return_to)
end
end
defmodule OpenStatusWeb.AuthController do
use OpenStatusWeb, :controller
use AshAuthentication.Phoenix.Controller

def success(conn, _activity, user, _token) do
return_to = get_session(conn, :return_to) || ~p"/"

conn
|> delete_session(:return_to)
|> store_in_session(user)
|> assign(:current_user, user)
|> redirect(to: return_to)
end

def failure(conn, _activity, reason) do
conn
|> assign(:failure_reason, reason)
|> put_status(401)
|> render("failure.html")
end

def sign_out(conn, _params) do
return_to = get_session(conn, :return_to) || ~p"/"

conn
|> clear_session()
|> redirect(to: return_to)
end
end
ZachDaniel
ZachDanielβ€’3y ago
So what does reason look like in that failure hook?
minib
minibOPβ€’3y ago
activity: {:github, :request}
reason: %AshAuthentication.Errors.MissingSecret{
resource: OpenStatus.Accounts.User,
changeset: nil,
query: nil,
error_context: [],
vars: [],
path: [:authentication, :strategies, :github, :client_id],
stacktrace: #Stacktrace<>,
class: :forbidden
}
activity: {:github, :request}
reason: %AshAuthentication.Errors.MissingSecret{
resource: OpenStatus.Accounts.User,
changeset: nil,
query: nil,
error_context: [],
vars: [],
path: [:authentication, :strategies, :github, :client_id],
stacktrace: #Stacktrace<>,
class: :forbidden
}
ZachDaniel
ZachDanielβ€’3y ago
πŸ™‚ there it is you're supposed to return {:ok, value} from secrets We should raise an error if it doesn't match. But yeah thats the problem.
minib
minibOPβ€’3y ago
ooo
ZachDaniel
ZachDanielβ€’3y ago
You can use fetch_env/2 for that
minib
minibOPβ€’3y ago
cool, thanks! now it redirects to GitHub but then it seems to conflict with password strategy or something like that
%Ash.Error.Changes.Required{
field: :hashed_password,
type: :attribute,
resource: OpenStatus.Accounts.User,
changeset: nil,
query: nil,
error_context: [],
vars: [],
path: [],
stacktrace: #Stacktrace<>,
class: :invalid
}
%Ash.Error.Changes.Required{
field: :hashed_password,
type: :attribute,
resource: OpenStatus.Accounts.User,
changeset: nil,
query: nil,
error_context: [],
vars: [],
path: [],
stacktrace: #Stacktrace<>,
class: :invalid
}
ZachDaniel
ZachDanielβ€’3y ago
I think you just need to make that attribute allow nil
minib
minibOPβ€’3y ago
should I allow_nil?: true for hashed_password?
ZachDaniel
ZachDanielβ€’3y ago
Yeah, if you want to support social sign ins
minib
minibOPβ€’3y ago
okay, now this error: ** (ArgumentError) No identity found for OpenStatus.Accounts.User called :email i guess I need to take some data from the user_info?
ZachDaniel
ZachDanielβ€’3y ago
Yep, you can see how we do it in ash_hq And you probably are missing an identity for email identity :unique_email, [:email]
change fn changeset, _ ->
user_info = Ash.Changeset.get_argument(changeset, :user_info)

changeset =
if user_info["email_verified"] do
Ash.Changeset.force_change_attribute(
changeset,
:confirmed_at,
Ash.Changeset.get_attribute(changeset, :confirmed_at) || DateTime.utc_now()
)
else
changeset
end

changeset
|> Ash.Changeset.change_attribute(:email, Map.get(user_info, "email"))
|> Ash.Changeset.change_attribute(:github_info, user_info)
end
change fn changeset, _ ->
user_info = Ash.Changeset.get_argument(changeset, :user_info)

changeset =
if user_info["email_verified"] do
Ash.Changeset.force_change_attribute(
changeset,
:confirmed_at,
Ash.Changeset.get_attribute(changeset, :confirmed_at) || DateTime.utc_now()
)
else
changeset
end

changeset
|> Ash.Changeset.change_attribute(:email, Map.get(user_info, "email"))
|> Ash.Changeset.change_attribute(:github_info, user_info)
end
minib
minibOPβ€’3y ago
hmm i have this already:
change fn changeset, _ ->
user_info = Ash.Changeset.get_argument(changeset, :user_info)
Ash.Changeset.change_attributes(changeset, Map.take(user_info, ["email"]))
end
change fn changeset, _ ->
user_info = Ash.Changeset.get_argument(changeset, :user_info)
Ash.Changeset.change_attributes(changeset, Map.take(user_info, ["email"]))
end
copied this from the guide
ZachDaniel
ZachDanielβ€’3y ago
Yeah, we should probably update it with my code snippet above Because the default set up the user will get a "confirm your email" email when signing up with github But if you combine my example with
unless user.confirmed_at do
if opts[:changeset] && opts[:changeset].action.name == :update_email do
AshHq.Accounts.Emails.deliver_update_email_instructions(
%{user | email: Ash.Changeset.get_attribute(opts[:changeset], :email)},
url(~p"/auth/user/confirm?confirm=#{token}")
)
else
AshHq.Accounts.Emails.deliver_confirmation_instructions(
user,
url(~p"/auth/user/confirm?confirm=#{token}")
)
end
end
unless user.confirmed_at do
if opts[:changeset] && opts[:changeset].action.name == :update_email do
AshHq.Accounts.Emails.deliver_update_email_instructions(
%{user | email: Ash.Changeset.get_attribute(opts[:changeset], :email)},
url(~p"/auth/user/confirm?confirm=#{token}")
)
else
AshHq.Accounts.Emails.deliver_confirmation_instructions(
user,
url(~p"/auth/user/confirm?confirm=#{token}")
)
end
end
in the confirmation sender, you can avoid that
minib
minibOPβ€’3y ago
I don't have email confitmations
ZachDaniel
ZachDanielβ€’3y ago
As for your error message, I think its just telling you that you're referring to an identity called :email somewhere that doesn't exist
minib
minibOPβ€’3y ago
i think it called :unique_email in my project
ZachDaniel
ZachDanielβ€’3y ago
Hmm...I guess it could be something in ash_authentication that is assuming your identity name but that doesn't sound likely I'd hunt around for somewhere you're using the :email atom where you should be using :unique_email
minib
minibOPβ€’3y ago
yeah I found that, all is good and working now! thanks a lot!
minib
minibOPβ€’3y ago
another minor issue: it seems that CSS is a bit off:
No description
minib
minibOPβ€’3y ago
I've added "../deps/ash_authentication_phoenix/**/*.ex" to assets/tailwind.config.js
ZachDaniel
ZachDanielβ€’3y ago
Yeah, so I think its reacting to your dark mode already but the overarching template doesn't set the background color I have this in an example app I'm working on
<body class={if @live_action == :sign_in, do: "bg-white dark:bg-black antialiased", else: "antialiased"}>
<body class={if @live_action == :sign_in, do: "bg-white dark:bg-black antialiased", else: "antialiased"}>
In the root Because I hadn't set up the rest of my app for dark mode, so I only wanted to set the bg to black on the sign_in pages
minib
minibOPβ€’3y ago
this helped, thanks!
ZachDaniel
ZachDanielβ€’3y ago
glad to help! Mind marking this as resolved and closing it? Feel free to open as many more as you want.
minib
minibOPβ€’3y ago
done

Did you find this page helpful?