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

Token based authentication with Phoenix #268

Closed
SimonLab opened this issue Feb 5, 2020 · 27 comments
Closed

Token based authentication with Phoenix #268

SimonLab opened this issue Feb 5, 2020 · 27 comments
Assignees
Labels
awaiting-review An issue or pull request that needs to be reviewed feedback Feedback from people using the App question A question needs to be answered before progress can be made on this issue technical A technical issue that requires understanding of the code, infrastructure or dependencies

Comments

@SimonLab
Copy link
Member

SimonLab commented Feb 5, 2020

The current authentication is using Phoenix session:

https://github.com/dwyl/app-mvp-phoenix/blob/d0b43ba3ee95bc292cdf4d79fffab5bfed36198a/lib/app_web/controllers/google_auth_controller.ex#L24-L41

and the login function:
https://github.com/dwyl/app-mvp-phoenix/blob/d0b43ba3ee95bc292cdf4d79fffab5bfed36198a/lib/app_web/controllers/auth_controller.ex#L24-L29

I'm looking now at how to create a token based authentication which can be used with an API and allowing clients (e.g Elm application) to authenticate with the Phoenix backend.

Looking at: dwyl/auth#38

@SimonLab SimonLab self-assigned this Feb 5, 2020
@nelsonic nelsonic added question A question needs to be answered before progress can be made on this issue technical A technical issue that requires understanding of the code, infrastructure or dependencies labels Feb 6, 2020
@nelsonic
Copy link
Member

nelsonic commented Feb 6, 2020

Hi @SimonLab, thanks for opening this issue. 🙌
I'm very keen to have a separate Phoenix App https://github.com/dwyl/auth to handle all auth requests.

image
Doc: docs.google.com/presentation/d/1PUKzbRQOEgHaOmaEheU7T3AHQhRT8mhGuqVKotEJkM0

The auth App could either be deployed as a separate/independent Phoenix App on a subdomain
e.g: auth.dwyl.com or deployed as an Umbrella App so that deployment is simplified.
But in both cases it should return a JWT to whichever app (Elixir or Elm) is invoking the endpoint.

This question is related to: dwyl/auth#34
We have been meaning to build https://github.com/dwyl/auth for a while ...
Part of that quest was getting dwyl/elixir-auth-github up to parity with dwyl/elixir-auth-google
The next step for auth is to return a JWT to the caller.
LMK if you want to pair on building this in https://github.com/dwyl/auth 💭

@SimonLab
Copy link
Member Author

SimonLab commented Feb 6, 2020

Here is my current understanding of the authentication (and authorisation) system:

  • The auth service is a Phoenix application in itself and not a package to add to the "main" application.
  • Steps of the request to allow a user to login with Google and create a task on the mvp app.

image

I guess it wasn't clear for me how the auth application and the main application are communicating, is the above diagram correct?

@nelsonic
Copy link
Member

nelsonic commented Feb 6, 2020

@SimonLab indeed from this diagram, it is not clear to me either how the 3 parts are communicating. 😉
What can you do to reduce the complexity in the diagram so that other people can understand it? 💭

@SimonLab
Copy link
Member Author

SimonLab commented Feb 6, 2020

I've been refreshing my memory by reading about JWT, the issues from the Auth repository and Phoenix umbrella app.

I'm very keen to have a separate Phoenix App https://github.com/dwyl/auth to handle all auth requests.

Having a specific application to manage the authentication/authorisation makes it independent from the main application and reduce the complexity of the code.
The same auth application can also be used for different "main" applications (mvp, home,...)

Now the question is how to achieve this. The issue dwyl/auth#25 mention using a Phoenix umbrella application. If we want to keep the complexity of the project low I don't think an umbrella app might be the right choice as it doesn't really decoupled the main app form the auth app:

image
https://elixir-lang.org/getting-started/mix-otp/dependencies-and-umbrella-projects.html#dont-drink-the-kool-aid

also https://elixirforum.com/t/phoenix-to-go-with-umbrella-or-not-new-project/16414

Another idea is to create a "normal" Phoenix application which will be used as an API to manage authentication, authorisation and user sessions. It's what I've tried to describe in the diagram above with the "Auth App". It's also what @RobStallion seems to have tested dwyl/auth#34 (comment)

I'm keen to go with the API Phoenix application for auth. I think we need first to clean the repository as we won't need for the first version Alog or dependencies like Ueberauth

@SimonLab
Copy link
Member Author

SimonLab commented Feb 6, 2020

Using elixir_auth_github and elixir_auth_google on the same project creates dependency conflict:
image

@nelsonic
Copy link
Member

nelsonic commented Feb 6, 2020

@SimonLab can you please open an issue for that so we can ensure that both packages use the same version? Thanks! ☀️

@nelsonic
Copy link
Member

nelsonic commented Feb 7, 2020

@SimonLab what do you think about this diagram:
(does it help understand the Token-based Auth Workflow?)
dwyl-auth-3rd-party-auth-workflow
docs.google.com/presentation/d/1PUKzbRQOEgHaOmaEheU7T3AHQhRT8mhGuqVKotEJkM0 Slide 2.

Workflow

  1. Client makes GET request for yourapp.com/auth endpoint/page
  2. YourApp notices that the request was made without a valid JWT so 401 redirects to /auth

Auth Service records the HTTP referrer in this case yourapp.com/auth

  1. Auth Service returns /auth page displaying the 3rd party "Sign in with X" buttons.
  2. Client clicks/taps on their desired "Sign in with X" button (e.g: Google) which makes the GET request to the 3rd Party OAuth Provider.

The 3rd Party OAuth knows what the /auth/{provider}/callback is.

  1. 3rd Party OAuth Provider displays the their Auth UI
  2. Client inputs valid credentials into 3rd party Auth UI (out of our control)
  3. 3rd Party OAuth Provider invokes the /auth/{provider}/callback
  4. Auth Service inserts the data into the relevant table. Creates a JWT with person_id.
    Then redirects back to referrer with the JWT.
  5. YourApp now has the JWT and can check if the person_id is permitted to view /admin page they originally requested in step 1 (above). If they are allowed to view it, display the /admin page. 🎉
  6. Auth Service invokes SES Lambda function to send "Welcome" email to Client. (Optional)

Please let me know if you have time for a quick Zoom call to discuss. (or if it's already clear, comment)
Thanks! ✨

@nelsonic
Copy link
Member

nelsonic commented Feb 7, 2020

Next I'm looking at: https://github.com/joken-elixir/joken 👀

@SimonLab
Copy link
Member Author

SimonLab commented Feb 7, 2020

The diagram on #268 (comment) makes sense. The only aspect I'll need to see more in detail is the role of the referrer and how to save it and use it.
I understand that it is useful when we want to redirect the user after authentication to the page she intended to access but I'll need to test it with Phoenix to see how it works.

@SimonLab
Copy link
Member Author

SimonLab commented Feb 7, 2020

I would also add the following flow, when a request to the app already contained a jwt. In this case the app needs to ask the auth service to check if the jwt is still valid. The app will get a 200 response if the user is still allowed to access the /admin endpoint and will return the page:
image

@nelsonic
Copy link
Member

nelsonic commented Feb 7, 2020

@SimonLab thanks for confirming that the diagram is clear. 👍
Indeed, need to have a JWT.validate function that can be invoked by YourApp to check if the JWT is valid and not expired. if the JWT is valid, we will have two options:

  1. Allow the next action if it is considered "harmless" i.e. create a new item.
  2. Request verification from Auth Service if the action is considered "destructive", e.g delete item

if the YourApp (the application is relying on Auth Service for security) is a highly sensitive App e.g. Banking, Healthcare or "Gov", then YourApp should always run Auth.verify to confirm that the JWT has not been invalidated. (this incurs an HTTP and DB request so it's much slower)

The hardest part of Token based Auth is this is Token invalidation.
e.g: when someone wants to "log out of all devices" because they suspect their device was lost/stolen.
I don't think we are going to address that during MVP.
It deserves a whole other issue (Epic?) because it needs good documentation and testing.

@SimonLab
Copy link
Member Author

SimonLab commented Feb 10, 2020

after testing the oauth flow with a client via our application API I realised that the last step when getting the oauth token, ie the redirect endpoint defined in your oauth application, won't reply automatically to the client.
So I'm not reading how to create a oauth flow from a client:

I will also look at the following Elm package: https://github.com/truqu/elm-oauth2/tree/6.0.0

@SimonLab
Copy link
Member Author

When creating credentials on Google oauth application we need to define the Authorized JavaScript origins instead of the Authorized redirect URIs:
image

@SimonLab
Copy link
Member Author

I've done more reading about the different options to let a user authenticate with Oauth.
For a single page application, the implicit grant flow seems promising (this is also the flow recommended by google for this kind of application).
image

Once the application receives the token we can then send this token to our API. It will then use this token to get the user information, create a jwt from this data and create a new session for the user.

@nelsonic
Copy link
Member

@SimonLab what is the difficulty with redirecting to the auth service as described in Step 7 above?

The "Implicit Flow" is for SPAs that don't have a way of storing secrets on the client.
We have a Phoenix server which is perfect for doing exactly this and simplifying the process.
We don't want the OAuth provider to redirect to the client because it creates extra steps and means we have to trust what the client says thus introducing a weak point in the auth process. We would only do this if we had no other option.

@SimonLab
Copy link
Member Author

SimonLab commented Feb 10, 2020

Yes I just realised that I over complicated the flow, not sure why I blocked on this and thought the backend wouldn't be able to send the response to the right client.

It's working well with a redirect and passing the jwt as a query parameter, for example the phoenix controller can be:

  def index(conn, %{"code" => code}) do
    {:ok, token} = ElixirAuthGoogle.get_token(code, conn)
    {:ok, profile} = ElixirAuthGoogle.get_user_profile(token.access_token)
    # Create auth token from profile email
    # Create session
    redirect(conn, external: "https://appelm.herokuapp.com?jwt=yeaaajwt.yeaaaa.aaaey")
    #render(conn, "index.json", profile: profile)
  end

I need now to remember how to manage routes with Elm: https://guide.elm-lang.org/webapps/navigation.html

@SimonLab
Copy link
Member Author

SimonLab commented Feb 12, 2020

One the previous comment, the code show how to send back a jwt from the api to the client with a redirect:

redirect(conn, external: "https://appelm.herokuapp.com?jwt=yeaaajwt.yeaaaa.aaaey")

However we want to avoid hardcoding the redirection url as different clients (i.e domain name) will be able to access. So we need a way to send the jwt to the correct client.

A way for the server to know where to redirect is to use the state query parameter which will contain the client url when creating the oauth urls:

    referer = case List.keyfind(conn.req_headers, "referer", 0) do
      {"referer", referer} ->
        referer
      nil ->
        IO.puts "no referer"
        nil
    end
    google_url = ElixirAuthGoogle.generate_oauth_url(conn) <> "&state=#{referer}"
    github_url = ElixirAuthGithub.login_url_with_scope(["user:email"]) <> "&state=#{referer}"

When the user click on the link, oauth will redirect to the callback url defined in the oauth application with the state query parameter. The api will then be able to redirect to this url:

  def index(conn, %{"code" => code, "state" => client}) do
    {:ok, token} = ElixirAuthGoogle.get_token(code, conn)
    {:ok, profile} = ElixirAuthGoogle.get_user_profile(token.access_token)

    redirect(conn, external: "#{state}?jwt=yeaaajwt.yeaaaa.aaaey")
  end

We can later on use another jwt for the state. This will allow the API server to verify the redirection url:

image

@SimonLab
Copy link
Member Author

I've been doing more testing this morning with 3 applications: a client build with Elm and two Phoenix applications, an API and an Auth.

I've tested specifically when the client try to access a restricted endpoint of the API, for example /api/admin, i.e. the following flow of the diagram:
image

First I've created a function in Elm which sends a get request to the API /api/admin endpoint:

accessAdmin : Cmd Msg
accessAdmin =
    Http.get
        { url = "http://localhost:3000/api/admin"
        , expect = Http.expectJson GotAdminData adminDataDecoder
        }

The API receives the request, check the jwt (not implemented here yet) and redirect to the Auth api application:

  def index(conn, _params) do
    # run code which verifies the jwt (add code in au plug?) 
    
    # if no jwt or not valid redirect the request to auth
    redirect(conn, external: "http://localhost:4000/api/auth")
  end

Finally the Auth app gets the request from the API and reply with the oauth urls:

  def index(conn, _params) do
    refer = get_referer(conn) # return the client hostname and and endpoint

    # user elixir_auth_google and elixir_auth_github
    urls = ["google_oauth_url&state=#{referer}", "github_oauth_url&state+#{referer}"]
    
    conn
    |> render("index.json", urls: urls)
  end

From there we can then display the urls in the Elm client.

The advantage of this flow is the client is doing only one request.
However depending on the status of the jwt the client will receive the admin data or the urls for authentication with oauth. In both case the status code of the reply is 200.
We can change the status of the reply, for example Auth can reply with a `401:

  def index(conn, _params) do
    refer = get_referer(conn) # return the client hostname and and endpoint

    # user elixir_auth_google and elixir_auth_github
    urls = ["google_oauth_url&state=#{referer}", "github_oauth_url&state+#{referer}"]
    
    conn
    |> put_status(401)
    |> render("index.json", urls: urls)
  end

However I haven't found an easy way to read the body of the response from a 401 with elm and only the error is returned:

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        Clicked ->
            ( model, accessAdmin )

        GotAdminData result ->
            case result of
                Ok data ->
                    ( data, Cmd.none )

                Err e ->
                    let
                        _ = Debug.log "error" e   -- can't read body response containing the oauth urls
                    in
                    
                    ( model, Cmd.none)

So I think that the we will need to define some endpoints specific for authentication also on the Api app. This endpoints will redirect to Auth but the client will now be able to ask for the urls and be able to manage the state of the application depending on the status code response. In this case the Api app expose the /auth endpoints to the client. This endpoints redirects then to the Auth /urls endpoint which will return directly the oauth urls to the client.

image

@SimonLab
Copy link
Member Author

Deployed to apps (app and auth) to make it easier to test redirects and authentication flow:
api-redirect

Here the app (dwyl-app-api) expose the /api/login url which let any client (in my case elm) ask for the oauth urls. The app redirect the request to auth which will generate the urls and return the result as json

@SimonLab
Copy link
Member Author

Using a Elm client which use the /api/login endpoint from the app.
Auth then automatically creates the oauth urls.
The user click on the github link and then is redirected to the application with a jwt:
redirect-jwt

@nelsonic
Copy link
Member

@SimonLab do we need to update our elixir-auth-github and elixir-auth-google to include a state property to pass the HTTP referrer data to the 3rd Party OAuth Providers ? 🛠
Or are you just appending it to the URL generated by the packages? 💭

@SimonLab
Copy link
Member Author

elixir_auth_github contains a function to add a state:
https://github.com/dwyl/elixir-auth-github/blob/03f390baec154ddbe991671ff90337132ce42e92/lib/elixir_auth_github.ex#L28-L30

However at the moment I'm concatenating the state to the url in the application.
Also I'm using this function, which takes scope but not the state:
https://github.com/dwyl/elixir-auth-github/blob/03f390baec154ddbe991671ff90337132ce42e92/lib/elixir_auth_github.ex#L36-L38

Maybe we could combine the functions by passing an Elixir map/struct containing all the query parameter we want to add:

%{scope: ["user:email"], state: "redirect to..."}

I'd like also to add an option to be able to specify the callback url. At the moment it is hardcoded in elixir_auth_google:
https://github.com/dwyl/elixir-auth-google/blob/71048683d52008bde0141e511ebd376d613dffd8/lib/elixir_auth_google.ex#L29-L31

@nelsonic
Copy link
Member

@SimonLab yeah, that login_url(state) function in elixir-auth-github
is kinda useless. It needs to be login_url(scopes, state).

@nelsonic nelsonic self-assigned this Apr 6, 2020
@nelsonic
Copy link
Member

This is taking shape in https://github.com/dwyl/auth_plug 🚧

@nelsonic
Copy link
Member

nelsonic commented May 1, 2020

The auth_plug repo is now in a reviewable state: https://github.com/dwyl/auth_plug 👍
As is the example: https://github.com/dwyl/auth_plug_example 🚀
And dwyl/auth#43 implements token based auth as a service. 🤯 😉

@nelsonic nelsonic added the awaiting-review An issue or pull request that needs to be reviewed label May 1, 2020
@nelsonic nelsonic removed their assignment May 1, 2020
@nelsonic
Copy link
Member

nelsonic commented Sep 8, 2020

@SimonLab as the original "owner" of this issue, can you confirm if the auth_plug satisfies your idea for Token-based Auth?
If not, can you please open any issues in https://github.com/dwyl/auth/issues with additional requirements? (Thanks!)

@nelsonic nelsonic added the feedback Feedback from people using the App label Sep 8, 2020
@nelsonic
Copy link
Member

GOTO: dwyl/auth#207

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
awaiting-review An issue or pull request that needs to be reviewed feedback Feedback from people using the App question A question needs to be answered before progress can be made on this issue technical A technical issue that requires understanding of the code, infrastructure or dependencies
Projects
None yet
Development

No branches or pull requests

2 participants