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

What's the correct way to enforce that users are logged in when visiting an URL? #54

Open
stabenfeldt opened this issue Apr 18, 2024 · 3 comments

Comments

@stabenfeldt
Copy link

stabenfeldt commented Apr 18, 2024

Hi,

The gen.auth project has a require_authenticated_user() plug that can be used.
I could not find anything here.

Can you guys please guide me on how the best security practices for implementing something similar?

@LuchoTurtle
Copy link
Member

LuchoTurtle commented Apr 18, 2024

Hey, thanks for opening an issue :)

This topic has a little bit that can be said about it, so I'll try to keep it short.

A foreword

It's important to recognize the difference between authorization and authentication.

  • authentication is all about identifying the user (who is he?).
  • authorization is all about knowing what the user can do (can he create/delete items?).

The Azure Identity platform does this like so:

The ** platform uses OAuth for authorization** and OpenID Connect (OIDC) for authentication. OpenID Connect is built on top of OAuth 2.0, so the terminology and flow are similar between the two. You can even both authenticate a user (through OpenID Connect) and get authorization to access a protected resource that the user owns (through OAuth 2.0) in one request.

So this package makes it possible for your web application to authorize itself to your app registration that you've defined in Azure AD. The latter uses the OpenID Connect protocol to do this.

What does gen.auth do?

From what I've gathered in their docs, when you use get.auth in your project, they create an Accounts schema where they store the sessions in the database. I think they bootstrap it with e-mail and password.

As per their own words, what you should do to enforce that users are logged in when visiting a URL, is to have this check on the mount/3 function. If there is no token that is valid, you redirect the user. Simple as. Here's a snippet from https://hexdocs.pm/phoenix_live_view/security-model.html#content.

def mount(_params, %{"user_id" => user_id} = _session, socket) do
  socket = assign(socket, current_user: Accounts.get_user!(user_id))

  socket =
    if socket.assigns.current_user.confirmed_at do
      socket
    else
      redirect(socket, to: "/login")
    end

  {:ok, socket}
end

I'll give you another example. We do the same thing in https://github.com/dwyl/learn-payment-processing. In this project, we only allow the user to see a page if they bought the product on Stripe.
Otherwise, we just redirect them to the home page.

 def success(conn, %{"session_id" => session_id}) do
    case Stripe.Checkout.Session.retrieve(session_id) do
      {:ok, session} ->
        person_id = conn.assigns.person.id

        UsersTable.create_user(%{
          person_id: person_id,
          stripe_id: session.customer,
          status: true
        })

        render(conn, :success, layout: false)

      {:error, _error} ->
        conn
        |> put_status(303)
        |> redirect(to: ~p"/")
    end
  end

Want another example? It's on our demo! :D

def welcome(conn, _params) do
# Check if there's a session token
case conn |> get_session(:token) do
# If not, we redirect the user to the login page
nil ->
conn |> redirect(to: "/")
# If there's a token, we render the welcome page
token ->
{:ok, profile} = ElixirAuthMicrosoft.get_user_profile(token.access_token)
conn
|> put_view(AppWeb.PageView)
|> render(:welcome, %{profile: profile, logout_microsoft_url: ElixirAuthMicrosoft.generate_oauth_url_logout()})
end
end

We simply check if there's a token to control who gets to see this URL.

But wait. What if the token is expired/invalid?

Yes, access tokens in Azure AD are short-lived and need to be refreshed if they are expired to continue accessing Azure resources. There refresh tokens usually have a longer lifetime.

As it stands, this package does not have a way for you to refresh the token.
I'll open an issue for that.

If you see in our demo, we just assume the token is valid and let it crash if it's not.

{:ok, profile} = ElixirAuthMicrosoft.get_user_profile(token.access_token)

You can handle the scenario with your token if the request fails and handle this error (usually redirecting them to the login page suffices, forcing them to get a new valid access token).

Until we have provided you a way to refresh tokens, you can still use get_token/2 to keep your web app alive in case a request fails because your current token is invalid.
So, if a request fails because the token is invalid, use get_token/2 to get a new one :).

Here's an image of what a usual flow with tokens will look like with your web application. Our package makes it easy to get those tokens and return them to you.
We´ll eventually provide you with the tools to refresh the token,
but it's up to you to handle the error scenarios. If a request fails because the token is invalid, you need to refresh the token or get a new one (this is temporary while we implement a function to refresh the token) or force the user to go to the home page to log in.

image

To give you another perspective, msal is Microsoft's official package for various frontend frameworks to make this process easy.
The way you're meant to check if a given person is already logged in and able to see the package is silently acquiring a token (which is checking if the token is valid) -> https://stackoverflow.com/questions/54346058/how-to-know-if-a-given-user-is-already-logged-in-with-msal

Alrighty. But what about roles? I want to allow a specific group of people to access a page and forbid others. How do I do that?

So far we've dealt with access tokens (used for authorization). The token returned by this package is the access token. ID Tokens can also be sent to the client application with claims with information about roles and permissions of the authorized user.

Microsoft's documentation are stellar. Read this for more information about the diff between tokens -> https://learn.microsoft.com/en-us/entra/identity-platform/security-tokens.

If you want to add roles and see them in your client application, you can control access at the App Registration-level by creating the roles there and making it so the roles are accessible in the token for you to see in the client application.

This goes a bit beyond the scope of what the package is now (though in the future it makes sense to have a an easy way to get the user's roles, even if you just need to JWT decode it) but to have role-based access on your application and have this information be returned on the access token, see https://learn.microsoft.com/en-us/answers/questions/1509291/(oidc)-configure-app-registration-to-return-roles.

TLDR;

Enforce the check on the mount/3 function to see if the session token exists. Redirect the user if to the login page if it's invalid/expired.

@ndrean
Copy link

ndrean commented Apr 18, 2024

Adding my one cent. For Liveview, one can rewritte this by using on_mount to "slightly clean" the code.

It can be used as the callback module of the live_session macro in the router, or declared directly (as a macro) in the Live component:.
Check this post

@stabenfeldt
Copy link
Author

Thanks for the good and detailed answers, @LuchoTurtle & @ndrean! ❤️
You gave me a good starting point.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants