Skip to content

Commit

Permalink
[identity] scope sessions by client
Browse files Browse the repository at this point in the history
  • Loading branch information
patatoid committed Jan 7, 2025
1 parent 56d036b commit 5a279ef
Show file tree
Hide file tree
Showing 6 changed files with 114 additions and 55 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,29 +9,32 @@ defmodule BorutaIdentityWeb.Authenticable do
# Make the remember me cookie valid for 60 days.
# If you want bump or reduce this value, also change
# the token expiry itself in UserToken.
@session_key :user_token
@max_age 60 * 60 * 24 * 60
@remember_me_cookie "_boruta_identity_web_user_remember_me"
@remember_me_options [sign: true, max_age: @max_age, same_site: "Lax"]

@spec remember_me_cookie() :: String.t()
def remember_me_cookie, do: @remember_me_cookie

@spec session_key(conn :: Plug.Conn.t()) :: String.t()
def session_key(conn) do
client_id_from_request(conn)
end

@spec store_user_session(conn :: Plug.Conn.t(), session_token :: String.t()) ::
conn :: Plug.Conn.t()
def store_user_session(%Plug.Conn{body_params: params} = conn, session_token) do
user = session_token && Accounts.get_user_by_session_token(session_token)

conn
|> assign(:current_user, user)
|> put_session(@session_key, session_token)
|> put_session(:live_socket_id, "users_sessions:#{Base.url_encode64(session_token)}")
|> put_session(session_key(conn), session_token)
|> maybe_write_remember_me_cookie(session_token, params["user"])
end

@spec get_user_session(conn :: Plug.Conn.t()) :: session_token :: String.t()
def get_user_session(conn) do
get_session(conn, @session_key)
get_session(conn, session_key(conn))
end

@spec remove_user_session(conn :: Plug.Conn.t()) :: conn :: Plug.Conn.t()
Expand All @@ -42,7 +45,7 @@ defmodule BorutaIdentityWeb.Authenticable do
end

defp maybe_write_remember_me_cookie(conn, token, %{"remember_me" => remember_me})
when remember_me in ["true", "on"] do
when remember_me in ["true", "on", true] do
put_resp_cookie(conn, @remember_me_cookie, token, @remember_me_options)
end

Expand Down Expand Up @@ -103,16 +106,24 @@ defmodule BorutaIdentityWeb.Authenticable do
end

@spec client_id_from_request(conn :: Plug.Conn.t()) :: String.t() | nil
def client_id_from_request(%Plug.Conn{query_params: query_params}) do
with {:ok, claims} <-
BorutaIdentityWeb.Token.verify(
query_params["request"] || "",
BorutaIdentityWeb.Token.application_signer()
),
{:ok, client_id} <- Map.fetch(claims, "client_id") do
client_id
else
_ -> nil
def client_id_from_request(conn) do
%Plug.Conn{query_params: query_params} = fetch_query_params(conn)

case query_params do
%{"client_id" => client_id} ->
client_id

%{"request" => request} ->
with {:ok, claims} <-
BorutaIdentityWeb.Token.verify(
request || "",
BorutaIdentityWeb.Token.application_signer()
),
{:ok, client_id} <- Map.fetch(claims, "client_id") do
client_id
else
_ -> nil
end
end
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ defmodule BorutaIdentityWeb.Sessions do

use BorutaIdentityWeb, :controller

import BorutaIdentityWeb.Authenticable, only: [remember_me_cookie: 0, after_sign_in_path: 1]
import BorutaIdentityWeb.Authenticable, only: [session_key: 1, remember_me_cookie: 0, after_sign_in_path: 1]

alias BorutaIdentity.Accounts

Expand All @@ -19,13 +19,13 @@ defmodule BorutaIdentityWeb.Sessions do
end

defp ensure_user_token(conn) do
if user_token = get_session(conn, :user_token) do
if user_token = get_session(conn, session_key(conn)) do
{user_token, conn}
else
conn = fetch_cookies(conn, signed: [remember_me_cookie()])

if user_token = conn.cookies[remember_me_cookie()] do
{user_token, put_session(conn, :user_token, user_token)}
{user_token, put_session(conn, session_key(conn), user_token)}
else
{nil, conn}
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ defmodule BorutaIdentityWeb.UserRegistrationControllerTest do
use BorutaIdentityWeb.ConnCase, async: true

import BorutaIdentity.AccountsFixtures
import BorutaIdentityWeb.Authenticable, only: [session_key: 1]

alias BorutaIdentity.Repo

Expand Down Expand Up @@ -51,7 +52,7 @@ defmodule BorutaIdentityWeb.UserRegistrationControllerTest do
"user" => %{"email" => email, "password" => valid_user_password()}
})

assert get_session(conn, :user_token)
assert get_session(conn, session_key(conn))
assert redirected_to(conn) =~ "/user_return_to"
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ defmodule BorutaIdentityWeb.UserSessionControllerTest do
use BorutaIdentityWeb.ConnCase, async: true

import BorutaIdentity.AccountsFixtures
import BorutaIdentityWeb.Authenticable, only: [session_key: 1]

alias BorutaIdentity.Repo

Expand All @@ -24,26 +25,20 @@ defmodule BorutaIdentityWeb.UserSessionControllerTest do
end

test "redirects if already logged in", %{conn: conn, user: user, request: request} do
conn = conn |> log_in(user) |> get(Routes.user_session_path(conn, :new, request: request))
assert redirected_to(conn) == "/user_return_to"
end
end

describe "POST /users/log_in" do
test "logs the user in with remember me", %{conn: conn, user: user, request: request} do
conn =
post(conn, Routes.user_session_path(conn, :create, request: request), %{
"user" => %{
"email" => user.username,
"password" => valid_user_password(),
"remember_me" => "true"
"password" => valid_user_password()
}
})

assert conn.resp_cookies["_boruta_identity_web_user_remember_me"]
assert redirected_to(conn) =~ "/"
conn = conn |> get(Routes.user_session_path(conn, :new, request: request))
assert redirected_to(conn) == "/user_return_to"
end
end

describe "POST /users/log_in" do
test "returns unauthorized when not confirmed", %{
conn: conn,
request: request,
Expand Down Expand Up @@ -82,10 +77,66 @@ defmodule BorutaIdentityWeb.UserSessionControllerTest do
"user" => %{"email" => user.username, "password" => valid_user_password()}
})

assert get_session(conn, :user_token)
assert get_session(conn, session_key(conn))
assert redirected_to(conn) == "/user_return_to"
end

test "scope the session by client", %{conn: conn, user: user, request: request} do
conn =
post(conn, Routes.user_session_path(conn, :create, request: request), %{
"user" => %{"email" => user.username, "password" => valid_user_password()}
})

assert get_session(conn, session_key(conn))
assert redirected_to(conn) == "/user_return_to"

identity_provider =
BorutaIdentity.Factory.insert(:identity_provider)

client = Boruta.Factory.insert(:client)

client_identity_provider =
BorutaIdentity.Factory.insert(:client_identity_provider,
identity_provider: identity_provider,
client_id: client.id
)

{:ok, request, _payload} =
Joken.encode_and_sign(
%{
"client_id" => client_identity_provider.client_id,
"scope" => "",
"user_return_to" => "/user_return_to"
},
BorutaIdentityWeb.Token.application_signer()
)

user = user_fixture(%{backend: identity_provider.backend})

conn =
post(conn, Routes.user_session_path(conn, :create, request: request), %{
"user" => %{"email" => user.username, "password" => valid_user_password()}
})

assert get_session(conn, session_key(conn))
assert redirected_to(conn) == "/user_return_to"
end

test "logs the user in with remember me", %{conn: conn, user: user, request: request} do
conn =
post(conn, Routes.user_session_path(conn, :create, request: request), %{
"user" => %{
"email" => user.username,
"password" => valid_user_password(),
"remember_me" => true
}
})

assert get_session(conn, session_key(conn))
assert redirected_to(conn) == "/user_return_to"
assert conn.resp_cookies["_boruta_identity_web_user_remember_me"]
end

test "logs the user in with totp identity provider", %{
conn: conn,
user: user,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ defmodule BorutaIdentityWeb.SessionsTest do

alias BorutaIdentityWeb.Sessions

@remember_me_cookie "_boruta_identity_web_user_remember_me"
setup :with_a_request

setup %{conn: conn} do
conn =
Expand All @@ -18,9 +18,19 @@ defmodule BorutaIdentityWeb.SessionsTest do
end

describe "fetch_current_user/2" do
test "authenticates user from session", %{conn: conn, user: user} do
test "authenticates user from session", %{
conn: conn,
client: client,
user: user,
request: request
} do
user_token = generate_user_session_token(user)
conn = conn |> put_session(:user_token, user_token) |> Sessions.fetch_current_user([])

conn =
%{conn | query_params: %{"request" => request}}
|> put_session(client.id, user_token)
|> Sessions.fetch_current_user([])

assert conn.assigns.current_user.id == user.id
end

Expand All @@ -30,24 +40,6 @@ defmodule BorutaIdentityWeb.SessionsTest do
refute get_session(conn, :user_token)
refute conn.assigns.current_user
end

test "authenticates user from cookies", %{conn: conn, user: user} do
logged_in_conn =
conn
|> fetch_cookies()
|> log_in(user, %{"user" => %{"remember_me" => "true"}})

user_token = logged_in_conn.cookies[@remember_me_cookie]
%{value: signed_token} = logged_in_conn.resp_cookies[@remember_me_cookie]

conn =
conn
|> put_req_cookie(@remember_me_cookie, signed_token)
|> Sessions.fetch_current_user([])

assert get_session(conn, :user_token) == user_token
assert conn.assigns.current_user.id == user.id
end
end

describe "redirect_if_user_is_authenticated/2" do
Expand Down
10 changes: 7 additions & 3 deletions apps/boruta_identity/test/support/conn_case.ex
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ defmodule BorutaIdentityWeb.ConnCase do
this option is not recommended for other databases.
"""

import BorutaIdentityWeb.Authenticable, only: [session_key: 1]

use ExUnit.CaseTemplate

alias BorutaIdentity.Accounts.User
Expand All @@ -23,6 +25,8 @@ defmodule BorutaIdentityWeb.ConnCase do
alias BorutaIdentityWeb.Authenticable
alias Ecto.Adapters.SQL.Sandbox

@request_client_id "a2836dfd-c2c7-4b13-a3c3-d408845d2ead"

using do
quote do
# Import conveniences for testing with connections
Expand Down Expand Up @@ -80,12 +84,12 @@ defmodule BorutaIdentityWeb.ConnCase do
It returns an updated `conn`.
"""
def log_in(conn, user, params \\ %{}) do
token = generate_user_session_token(user)
session_token = generate_user_session_token(user)

conn
|> Phoenix.ConnTest.init_test_session(%{})
|> Map.put(:body_params, params)
|> Authenticable.store_user_session(token)
|> Plug.Conn.put_session(@request_client_id, session_token)
end

def with_a_request(_params) do
Expand All @@ -98,7 +102,7 @@ defmodule BorutaIdentityWeb.ConnCase do
backend: BorutaIdentity.Factory.build(:smtp_backend)
)

client = Boruta.Factory.insert(:client)
client = Boruta.Factory.insert(:client, id: @request_client_id)
scope = Boruta.Factory.insert(:scope, name: "request:scope", label: "Scope from request")

client_identity_provider =
Expand Down

0 comments on commit 5a279ef

Please sign in to comment.