oauth/oidc

This commit is contained in:
bluepython508
2023-11-07 19:35:03 +00:00
parent a0fc306df1
commit 54db8727b0
20 changed files with 670 additions and 3 deletions

View File

@@ -0,0 +1,159 @@
defmodule SsoBsnWeb.Openid.AuthorizeController do
@behaviour Boruta.Oauth.AuthorizeApplication
use SsoBsnWeb, :controller
alias Boruta.Oauth.AuthorizeResponse
alias Boruta.Oauth.Error
alias Boruta.Oauth.ResourceOwner
alias SsoBsnWeb.UserAuth
alias SsoBsnWeb.Openid.AuthorizeView
def oauth_module, do: Application.get_env(:sso_bsn, :oauth_module, Boruta.Oauth)
def authorize(%Plug.Conn{} = conn, _params) do
conn =
conn
|> store_user_return_to()
|> put_unsigned_request()
resource_owner = get_resource_owner(conn)
with {:unchanged, conn} <- prompt_redirection(conn),
{:unchanged, conn} <- max_age_redirection(conn, resource_owner),
{:unchanged, conn} <- login_redirection(conn) do
oauth_module().authorize(conn, resource_owner, __MODULE__)
end
end
@impl Boruta.Oauth.AuthorizeApplication
def authorize_success(
conn,
%AuthorizeResponse{} = response
) do
redirect(conn, external: AuthorizeResponse.redirect_to_url(response))
end
@impl Boruta.Oauth.AuthorizeApplication
def authorize_error(
%Plug.Conn{} = conn,
%Error{status: :unauthorized, error: :login_required} = error
) do
redirect(conn, external: Error.redirect_to_url(error))
end
def authorize_error(
%Plug.Conn{} = conn,
%Error{status: :unauthorized, error: :invalid_resource_owner}
) do
redirect_to_login(conn)
end
def authorize_error(
conn,
%Error{
format: format
} = error
)
when not is_nil(format) do
redirect(conn, external: Error.redirect_to_url(error))
end
def authorize_error(
conn,
%Error{status: status, error_description: error_description}
) do
conn
|> put_status(status)
|> text(error_description)
end
defp put_unsigned_request(%Plug.Conn{query_params: query_params} = conn) do
unsigned_request_params =
with request <- Map.get(query_params, "request", ""),
{:ok, params} <- Joken.peek_claims(request) do
params
else
_ -> %{}
end
query_params = Map.merge(query_params, unsigned_request_params)
%{conn | query_params: query_params}
end
defp store_user_return_to(conn) do
# remove prompt and max_age params affecting redirections
conn
|> put_session(
:user_return_to,
current_path(conn)
|> String.replace(~r/prompt=(login|none)/, "")
|> String.replace(~r/max_age=(\d+)/, "")
)
end
defp prompt_redirection(%Plug.Conn{query_params: %{"prompt" => "login"}} = conn), do: log_out_user(conn)
defp prompt_redirection(%Plug.Conn{} = conn), do: {:unchanged, conn}
defp max_age_redirection(
%Plug.Conn{query_params: %{"max_age" => max_age}} = conn,
%ResourceOwner{} = resource_owner
) do
case login_expired?(resource_owner, max_age) do
true ->
log_out_user(conn)
false ->
{:unchanged, conn}
end
end
defp max_age_redirection(%Plug.Conn{} = conn, _resource_owner), do: {:unchanged, conn}
defp login_expired?(%ResourceOwner{last_login_at: last_login_at}, max_age) do
now = DateTime.utc_now() |> DateTime.to_unix()
with "" <> max_age <- max_age,
{max_age, _} <- Integer.parse(max_age),
true <- now - DateTime.to_unix(last_login_at) >= max_age do
true
else
_ -> false
end
end
defp login_redirection(%Plug.Conn{assigns: %{current_user: _current_user}} = conn) do
{:unchanged, conn}
end
defp login_redirection(%Plug.Conn{query_params: %{"prompt" => "none"}} = conn) do
{:unchanged, conn}
end
defp login_redirection(%Plug.Conn{} = conn) do
redirect_to_login(conn)
end
defp get_resource_owner(conn) do
case conn.assigns[:current_user] do
nil ->
%ResourceOwner{sub: nil}
current_user ->
%ResourceOwner{
sub: to_string(current_user.id),
username: current_user.username,
last_login_at: current_user.last_login_at
}
end
end
defp redirect_to_login(conn) do
conn |> redirect(to: ~p"/users/log_in")
end
defp log_out_user(conn) do
UserAuth.log_out_user(conn)
end
end

View File

@@ -0,0 +1,11 @@
defmodule SsoBsnWeb.Openid.ConfigurationController do
use SsoBsnWeb, :controller
def config(conn, _params) do
conn |> json(%{
issuer: url(~p"/"),
authorization_endpoint: url(~p"/openid/authorize"),
token_endpoint: url(~p"/oauth/token")
})
end
end

View File

@@ -0,0 +1,16 @@
defmodule SsoBsnWeb.Openid.JwksController do
@behaviour Boruta.Openid.JwksApplication
use SsoBsnWeb, :controller
def openid_module, do: Application.get_env(:sso_bsn, :openid_module, Boruta.Openid)
def jwks_index(conn, _params) do
openid_module().jwks(conn, __MODULE__)
end
@impl Boruta.Openid.JwksApplication
def jwk_list(conn, jwk_keys) do
conn |> json(%{keys: jwk_keys})
end
end

View File

@@ -0,0 +1,30 @@
defmodule SsoBsnWeb.Openid.UserinfoController do
@behaviour Boruta.Openid.UserinfoApplication
use SsoBsnWeb, :controller
alias Boruta.Openid.UserinfoResponse
def openid_module, do: Application.get_env(:sso_bsn, :openid_module, Boruta.Openid)
def userinfo(conn, _params) do
openid_module().userinfo(conn, __MODULE__)
end
@impl Boruta.Openid.UserinfoApplication
def userinfo_fetched(conn, userinfo_response) do
conn
|> put_resp_header("content-type", UserinfoResponse.content_type(userinfo_response))
|> json(UserinfoResponse.payload(userinfo_response))
end
@impl Boruta.Openid.UserinfoApplication
def unauthorized(conn, error) do
conn
|> put_resp_header(
"www-authenticate",
"error=\"#{error.error}\", error_description=\"#{error.error_description}\""
)
|> send_resp(:unauthorized, "")
end
end