elixir-auth-google
elixir-auth-google copied to clipboard
Errors: Handle Auth Failure Conditions
As a person using the @dwyl app to be more personally effective, I do not want to stumble at the first hurdle trying to authenticate. I don't expect to see unfriendly/unrecoverable error messages, rather I expect to be able to recover from errors without drama.
At present our get_token/1 function is only following the "happy path":
https://github.com/dwyl/elixir-auth-google/blob/687fba552db8b5de2d353d42f77e2f04d9d428f9/lib/elixir_auth_google.ex#L18-L29
The callback function is parse_body_response/1 which does not handle the {:error, err}
https://github.com/dwyl/elixir-auth-google/blob/687fba552db8b5de2d353d42f77e2f04d9d428f9/lib/elixir_auth_google.ex#L38-L45
This is fine during MVP because as early "dogfooding" users we are tolerant of the failure/errors. But as soon as we ship and show the MVP to (friendly) alpha test users, we need to have this done.
Todo:
- [ ] Research and document the various failure conditions and HTTP status codes
- [ ] Add failure
casestatement to handle all the error conditions. - [ ] Update the
templateto ensure that error conditions are displayed in a friendly way. - [ ] Update instructions in
README.md> How? section to inform people about error scenarios. - [ ] Re-publish the hex.pm package with the error handling.
We do not need to handle the failures before we ship our MVP, let's come back to this when it's needed.
All errors are 400;401 and 404 depending on the bad input. You can consider this possible solution:
def parse_response({:error, response}), do: {:error, response}
def parse_response({:ok, response}), do: get_user_profile(response.access_token)
def parse_status({:ok, %{status_code: 200}} = response), do: parse_body_response(response)
def parse_status({:ok, _}), do: {:error, :bad_input}
# or more verbose
def parse_status(request) do
case request do
{:ok, %{status_code: 200} = response} ->
parse_body_response({:ok, response})
{:ok, %{status_code: 404}} ->
{:error, :wrong_url}
{:ok, %{status_code: 401}} ->
{:error, :unauthorized_with_bad_secret}
{:ok, %{status_code: 400}} ->
{:error, :bad_code}
end
end
and use it: (get_token refactored to get_profile to expose only one function)
def get_profile(code, conn) do
Jason.encode!(%{
client_id: google_client_id(),
client_secret: google_client_secret(),
redirect_uri: generate_redirect_uri(conn),
grant_type: "authorization_code",
code: code
})
|> then(fn body ->
inject_poison().post(@google_token_url, body)
|> parse_status()
|> parse_response()
end)
end
defp get_user_profile(access_token) do
access_token
|> encode()
|>then(fn params ->
(@google_user_profile <> "?" <> params)
|> inject_poison().get()
|> parse_status()
end)
end
defp encode(token), do: URI.encode_query(%{access_token: token}, :rfc3986)
defp parse_body_response({:error, err}), do: {:error, err}
defp parse_body_response({:ok, %{body: nil}}), do: {:error, :no_body}
defp parse_body_response({:ok, %{body: body}}) do
{:ok,
body
|> Jason.decode!()
|> convert()}
end
defp convert(str_key_map) do
for {key, val} <- str_key_map, into: %{}, do: {String.to_atom(key), val}
end