Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Google OAuth Authentication #247

Closed
15 tasks done
nelsonic opened this issue Nov 18, 2019 · 37 comments
Closed
15 tasks done

Google OAuth Authentication #247

nelsonic opened this issue Nov 18, 2019 · 37 comments
Assignees
Labels
discuss Share your constructive thoughts on how to make progress with this issue enhancement New feature or enhancement of existing functionality epic A feature idea that is large enough to require a sprint (5 days) or more and has smaller sub-issues. priority-2 Second highest priority, should be worked on as soon as the Priority-1 issues are finished question A question needs to be answered before progress can be made on this issue T1d Time Estimate 1 Day T4h Time Estimate 4 Hours technical A technical issue that requires understanding of the code, infrastructure or dependencies

Comments

@nelsonic
Copy link
Member

nelsonic commented Nov 18, 2019

As a person who has a Google Account (for personal or work reasons),
I want the ability sign into the @dwyl App using my Google Account
so that I don't have to waste time registering or remembering another password.

image

Todo

We have implemented this before: https://github.com/dwyl/hapi-auth-google this can be used as a reference.

@nelsonic nelsonic added enhancement New feature or enhancement of existing functionality discuss Share your constructive thoughts on how to make progress with this issue priority-2 Second highest priority, should be worked on as soon as the Priority-1 issues are finished technical A technical issue that requires understanding of the code, infrastructure or dependencies labels Nov 18, 2019
@nelsonic nelsonic added this to the Sprint 1 - Authentication milestone Nov 18, 2019
@nelsonic nelsonic mentioned this issue Nov 18, 2019
1 task
@SimonLab SimonLab self-assigned this Nov 18, 2019
@SimonLab SimonLab added T1d Time Estimate 1 Day T4h Time Estimate 4 Hours in-progress An issue or pull request that is being worked on by the assigned person labels Nov 18, 2019
@SimonLab
Copy link
Member

SimonLab commented Nov 18, 2019

Recap on how "Sign in with Google" works

Google provide OAuth protocol to allow user to signin with an application.
OAuth allow users first to identify themself with their Google credentials, this is the authorization step.
Google then create a token for each users which can be used by the application (ie dwyl app ian our case) to access Google API endpoints on behalf of the users. The tokens let the application know that the user is a "real user" (ie users' credentials valid) and the application can now authenticate the users.

The Google documentation ( Using OAuth 2.0 to Access Google APIs) contains the following schemas which describe in a bit more details the steps of authorization:

image

So once a Google token is created for a user, the application will create a session for the user with put_session function (note that the SECRET_KEY_BASE is used to encrypt the session)

Because the signin process will create a google token and a client session we need to remember to verify that both of this items are still valid and especially checking that they have not expired.

@SimonLab
Copy link
Member

SimonLab commented Nov 19, 2019

I'm currently testing the elixir_auth_google package on the application by adding it as a dependency using the Github link and branch name:

  defp deps do
    [
      {:phoenix, "~> 1.4.10"},
      {:phoenix_pubsub, "~> 1.1"},
      {:phoenix_ecto, "~> 4.0"},
      {:ecto_sql, "~> 3.1"},
      {:postgrex, ">= 0.0.0"},
      {:phoenix_html, "~> 2.11"},
      {:phoenix_live_reload, "~> 1.2", only: :dev},
      {:gettext, "~> 0.11"},
      {:jason, "~> 1.0"},
      {:plug_cowboy, "~> 2.0"},
      {:elixir_auth_google, git: "https://github.com/dwyl/elixir-auth-google.git", branch: "initialise-app" }
    ]
  end

⚠️ if the code on the branch has been updated mix deps.get will not get the new code automatically. You need to force mix to get the latest change (deleting the deps folder and run mix deps.get force the dependencies to be udpated)

@nelsonic
Copy link
Member Author

@SimonLab indeed because the git dependency doesn't have a version/hash updates are manual. 😞 (hopefully not too tedious until we are able to publish it to Hex.pm...) 👍

@SimonLab
Copy link
Member

Now that we have a working elixir package to get a Google token with OAuth we can create a session with Phoenix for the user.

  • On first signin get Google token and save it in Postgres in the user table along the user profile information
  • Create a Pheonix session
  • If the phoenix session has expired, The user needs to signin with Google again. A new token will be created and will replace the old one in the database.
  • If the google token lifetime has expired, we can use the refresh_token (which should also be saved in Postgres) to get a new valid token.

@SimonLab
Copy link
Member

How to save the token in the Google database?
I think we should hash the token when saving them but we also need a way to get the original token from the database to be able to use it with for the Google API. We have use comeonin to hash password on previous Phoenix applications however I don't this this let you get back the original token.

@nelsonic
Copy link
Member Author

@SimonLab sounds like we need a new schema for storing this data.
I'm very keen to develop https://github.com/dwyl/fields and publish to Hex.pm
so that we can use Fields.Encrypted to handle the encryption, storage and decryption of tokens.
It will handle key rotation/expiry which you don't get "built-in" in a library like comeonin.

Note: Some people feel that storing Auth Tokens in a database is a bad idea because if malicious user were to gain access to the db, they could access a lot more data on the other services.
see this thread: https://security.stackexchange.com/questions/72475/should-we-store-accesstoken-in-our-database-for-oauth2 but I feel that if we encrypt keys appropriately and never store them in JS/Elm land (e.g in localStorage... see: https://auth0.com/docs/security/store-tokens ) then we can maintain a good balance between security and convenience for the users.

@SimonLab
Copy link
Member

Agree for having the fields package to manage the common schema data.
I'll try to add the package with the github url in the app project.
From the Readme I think we can use the Encrypted field for the token and it seems that we can then get the original token to be able to use it with Github APIs (when necessary):
https://github.com/dwyl/fields/blob/c0ccb6a9c835590a3d1432f20323aedd5c44ce59/lib/encrypted.ex#L26-L28

see also the Fields.AES module:
https://github.com/dwyl/fields/blob/master/lib/aes.ex

SimonLab added a commit to dwyl/mvp that referenced this issue Nov 21, 2019
@SimonLab
Copy link
Member

Preparing the application for Fields

  • add a new migration for adding the token_hash and refresh_token_hash fields in the people table`
  • add fields dependency in the application using git url
  • update App.Ctx.People schema to add the token and refresh_token fields using the encrypted type
  • Define encryption keys in the config. see https://github.com/dwyl/fields#config (I'm going to research how to create the encryptions keys)
    • do we need multiple keys
    • how to create these keys

@SimonLab
Copy link
Member

Find an example on the Fields repo for the encryption keys:
https://github.com/dwyl/fields/blob/master/.env_sample

So it looks like we can create multiple keys by seperating them with comma and it looks like they are created with :base64:

Enum.map(fn key -> :base64.decode(key) end)

@nelsonic
Copy link
Member Author

@SimonLab progress looking good. 👍
I would suggest creating a session schema/table to store token related data and linking it to people to have a separation of concerns. Also, token_hash can just be auth_token
and refresh_token_hash can be refresh_token because technically we aren't hashing the values. (We're encrypting them so they can be decrypted if needed.)

@SimonLab
Copy link
Member

sounds good @nelsonic
So instead of updating the people table will create

  • a session table with auth_token, refresh_token and people_id fields
  • a schema using Field and referencing the people schema with has_one

Other notes

@nelsonic
Copy link
Member Author

@SimonLab we may also need a key_id:integer field in session to store the ID of the encryption key.
That way we know which key was used to encrypt a piece of data and can be used to decrypt it.
The need for key_id field is described in https://github.com/dwyl/phoenix-ecto-encryption-example

@SimonLab
Copy link
Member

image

@nelsonic
Copy link
Member Author

According to branding guidelines: https://developers.google.com/identity/branding-guidelines#color
There are two colors the "Sign in with Google" button is allowed to have:
white #FFFFFF and blue #4285F4
image

Which color of button do we prefer?
@iteles should we pick one color for the button for MVP and .then figure out how to A/B test it later?

@nelsonic nelsonic added the question A question needs to be answered before progress can be made on this issue label Nov 26, 2019
@iteles
Copy link
Member

iteles commented Nov 26, 2019

@nelsonic Exactly that. Let's go with blue for now so it's a quick spot?

@SimonLab
Copy link
Member

While installing Fields for creating session schema a conflict in the mix dependencies occurs:
image

@SimonLab
Copy link
Member

We can force the use of a specific version of a dependency using the override option. For example adding

 {:poison, "~> 2.2", override: true},

will make sure the application uses poison 2.2
However this might not be the best solution as some function using a newest version of poison might break. I think it is better to update the dependency in the package directly if possible

@SimonLab
Copy link
Member

Working on the following logic when google returns the user profile:

  • get the user by email
  • if user doesn't exist
    • create user (define a new changeset as we don't have any password to save yet)
    • create session (if a user doesn't exist yet a session shouldn't exists, so we need to make sure that when a user is deleted completely the session attached to the user is also deleted)
  • if user exists
    • Update user information? I don't think google user information should replace the information of the app, ie the user might have updated her information on the app which might be different to the one on google.
    • Get session linked to user
      • If session exits do we want to update the tokens on this session or do we want to create a completely new session and invalidate the old sesssion. Creating a new session allow us to keep an history of all the user's sessions
    • If session doesn't exist, create a new session

I need to simplify the flow and then add the details step by step as it becomes difficult to test all the cases
So reworking on just getting the person by email then creating a new session.

@SimonLab
Copy link
Member

Reviewing how to work with association with Ecto. I've defined an association in the Sessions schema with belongs_to referencing the Person schema, however I'm wondering if 'has_many' or 'has_oneis also required to be defined in thePerson` schema

From the documentation it seems that the has_many or has_one is often added but doesn't say it is required:
image

watching
https://www.youtube.com/watch?v=THUG8J3xSYw

@nelsonic
Copy link
Member Author

nelsonic commented Nov 27, 2019

@SimonLab thanks for sharing your plan. 👍
A person can have many sessions so that might be an option.
But I don't feel that we need to enforce any sort of constraint.

Remember that the Using Ecto Associations in Phoenix Elixir Casts video is from Jan 2017 which includes "models" which are no longer part of latest Phoenix. The ecto association is still relevant.

@nelsonic
Copy link
Member Author

@SimonLab any objection to me changing the Google Auth callback url
from /google-auth-callback to /auth/google/callback
so that we create a name-spacing in the form /auth (scope) /google (provider) /callback (action)
and we can use it later for other providers e.g: /auth/basic/login or /auth/basic/register
and then when we get fancy and support "Sign in with Apple" (because Apple users are the ones who actually pay to use Apps) it would be /auth/apple/callback

I agree that this is semantic. But if we have some reasoning behind our URL patterns,
then we can "hard code" them into our elixir-auth-google package and reduce the complexity of the package because people will no longer have to define their callback URL in config.exs it will be the same URL for any app and they just have to implement their own custom controller linked to the route.

LMK your thoughts. 👍

@nelsonic
Copy link
Member Author

nelsonic commented Nov 30, 2019

The profile data returned by Google Auth is:

profile: %{
  "email" => "[email protected]",
  "email_verified" => true,
  "family_name" => "Correia",
  "given_name" => "Nelson",
  "locale" => "en",
  "name" => "Nelson Correia",
  "picture" => "https://lh3.googleusercontent.com/a-/AAuE7mApnYb260YC1JY7aPUBxwk8iNzVKB5Q3x_8d3-ThA",
  "sub" => "940732358705212133793"
}

We need to store all of this data in the person record so that we can customise the person's experience.
It means adding the missing fields to the person schema:

  • locale - the language the UI (_we will eventually translate the UI into multiple languages ...
  • picture - the person's google account profile picture, useful to display in the app for personalisation.
  • sub - the id of the google account. do we need to store this info or is the email address enough?

We will use the email_verified as status=1
for people registering to use the App with their email, status=0
until they verify their email address by clicking the verification link we send them by email. 🔗
In the case of a Google Account, the email is already verified. ✅

@nelsonic nelsonic added the epic A feature idea that is large enough to require a sprint (5 days) or more and has smaller sub-issues. label Nov 30, 2019
@nelsonic
Copy link
Member Author

nelsonic commented Nov 30, 2019

Going to add the two missing fields locale and picture to people schema:

mix ecto.gen.migration add_picture_locale_to_people
defmodule App.Repo.Migrations.AddPictureLocaleToPeople do
  use Ecto.Migration

  def change do
    alter table(:people) do
      add :picture, :binary # Field.Url
      add :locale, :string, default: "en"
    end
  end
end

@nelsonic
Copy link
Member Author

Google Profile data saved to people table:
image

@nelsonic
Copy link
Member Author

nelsonic commented Dec 2, 2019

Almost there ...
image

@SimonLab
Copy link
Member

SimonLab commented Dec 2, 2019

@nelsonic , yes the name of the callback endpoint is a bit more flexible with /auth/google/callback (#247 (comment))

On the elixir-auth-google we can define a default callback endpoint. I'm wondering if there are some cases where the user still want to define it manually, we can add back this option later on if necessary.

In fact I'm not sure we can hard code the url of the callback endpoint. I was thinking to call a function to get the current hostname (eg http://localhost:4000) then concatenate /auth/google/callback, however elixir-auth-google doesn't use Phoenix or Plug which would let us do this. The user could pass the hostname to the config but we then we just switch the callback config to a hostname config

@nelsonic
Copy link
Member Author

nelsonic commented Dec 2, 2019

@SimonLab exactly. I feel that having it "standardised" will help people move faster.
In the future, if someone really needs to customise the redirect URL, we can add that (with tests).
For an example of a reasonably "successful" open source project that has evolved a lot,
take a look at: https://github.com/dwyl/hapi-auth-jwt2 which has followed the "sensible defaults" convention and has many customisations submitted by the community while maintaining full backward compatibility.

As for the getting the hostname. See: dwyl/elixir-auth-google#17 and PR: dwyl/elixir-auth-google#18
We must do everything we can to make this package as easy to use for complete beginners as possible so that anyone can add "Sign-in with Google" to their Elixir/Phoenix App in 5 mins. 👍

@nelsonic
Copy link
Member Author

nelsonic commented Dec 4, 2019

@SimonLab what do you feel is still required to consider this issue/feature "done"? 💭

@SimonLab
Copy link
Member

SimonLab commented Dec 4, 2019

I'm currently adding dwyl/mvp#36 which redirect the user to her info page if already loggedin.

I'd like to add a logout link to the user info page also. And finally I'd like to verify that all the person information is saved correctly. I think the status value is not done correctly. However these points might be added later on and as this PR is already important (number of lines) I think it might be ready for review/merging. But let me know if you want these last items done first ⬆️

@nelsonic
Copy link
Member Author

nelsonic commented Dec 4, 2019

@SimonLab makes sense to do dwyl/app-mvp-phoenix#36 given your time associated T2h time estimate on dwyl/mvp#34 ... best to just get it done now. 👍 (assign when ready. thanks!)

@SimonLab
Copy link
Member

SimonLab commented Dec 4, 2019

The last point to finish for this PR is to define the status for the email (verified or not).

status=1 the email address has been verified.

The person table creates link to the status table with a foreign key on the field status. So we need first to define the verified_email and unverified_email value in the status table with a seed file.
Then when a new person is created with the Google authentication flow we can set the association with the id referencing verified_email

seeding-data Phoenix article

@nelsonic

This comment has been minimized.

@SimonLab
Copy link
Member

With Google authentication we are saving in the Session table the google token and refresh_token.
Currently the PR dwyl/mvp#42 which allow people to login/register with an email and password is saving the email hash and password hash directly on the People table.
At this stage the application doesn't use the session however it will in a new future, so I'm wondering if we also want to save a session for the email/password authentication and which information do we want to save, @nelsonic do you have a preference?

@nelsonic
Copy link
Member Author

@SimonLab this sounds like a question for the #237 issue. 😉
And perhaps it's worth having a look at the Session Management EPIC dwyl/auth#30 💭

@nelsonic
Copy link
Member Author

This is now working on https://dwylapp.herokuapp.com
image

Closing. ✅

@SimonLab SimonLab removed the in-progress An issue or pull request that is being worked on by the assigned person label Jan 22, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
discuss Share your constructive thoughts on how to make progress with this issue enhancement New feature or enhancement of existing functionality epic A feature idea that is large enough to require a sprint (5 days) or more and has smaller sub-issues. priority-2 Second highest priority, should be worked on as soon as the Priority-1 issues are finished question A question needs to be answered before progress can be made on this issue T1d Time Estimate 1 Day T4h Time Estimate 4 Hours technical A technical issue that requires understanding of the code, infrastructure or dependencies
Projects
None yet
Development

No branches or pull requests

3 participants