151 lines
5.2 KiB
Elixir
151 lines
5.2 KiB
Elixir
defmodule ComfycampWeb.OauthController do
|
||
use ComfycampWeb, :controller
|
||
|
||
alias Comfycamp.Accounts
|
||
alias Comfycamp.SSO
|
||
alias Comfycamp.SSO.OIDCApp
|
||
alias Comfycamp.SSO.OIDCCode
|
||
alias Comfycamp.SSO.IDToken
|
||
alias Comfycamp.Token
|
||
|
||
@doc """
|
||
Check the request parameters and current user status,
|
||
then ask the user to confirm that he wants to share his info with Relying Party.
|
||
"""
|
||
def authorize(conn, %{"client_id" => client_id, "redirect_uri" => redirect_uri} = params) do
|
||
app = SSO.get_oidc_app!(client_id)
|
||
current_user = conn.assigns.current_user
|
||
|
||
with {:is_approved, true} <- {:is_approved, current_user.is_approved},
|
||
{:response_type, "code"} <- {:response_type, params["response_type"]},
|
||
{:is_enabled, true} <- {:is_enabled, app.enabled},
|
||
{:has_uri, true} <- {:has_uri, SSO.has_redirect_uri?(client_id, redirect_uri)} do
|
||
render(conn, :authorize,
|
||
page_title: "Подтвердите вход",
|
||
app_name: app.name,
|
||
params: URI.encode_query(params)
|
||
)
|
||
else
|
||
{:is_approved, false} ->
|
||
render(conn, :error,
|
||
description:
|
||
"Ваш аккаунт ещё не был одобрен, подождите немного или свяжитесь с администратором"
|
||
)
|
||
|
||
{:response_type, response_type} ->
|
||
render(conn, :error, description: "Неподдерживаемый response type: #{response_type}")
|
||
|
||
{:is_enabled, false} ->
|
||
render(conn, :error, description: "Приложение отключено")
|
||
|
||
{:has_uri, false} ->
|
||
render(conn, :error, description: "Redirect URI не зарегистрирован или отсутствует")
|
||
end
|
||
end
|
||
|
||
@doc """
|
||
Generate an Authorization Code and redirect the user back to Relying Party.
|
||
"""
|
||
def generate_code(conn, %{"client_id" => client_id, "redirect_uri" => redirect_uri} = params) do
|
||
app = SSO.get_oidc_app!(client_id)
|
||
current_user = conn.assigns.current_user
|
||
|
||
with {:is_approved, true} <- {:is_approved, current_user.is_approved},
|
||
{:is_enabled, true} <- {:is_enabled, app.enabled},
|
||
{:has_uri, true} <- {:has_uri, SSO.has_redirect_uri?(client_id, redirect_uri)} do
|
||
{:ok, code} =
|
||
SSO.create_oidc_code(%{
|
||
oidc_app_id: client_id,
|
||
user_id: conn.assigns.current_user.id,
|
||
redirect_uri: redirect_uri
|
||
})
|
||
|
||
uri = build_redirect_uri(redirect_uri, code.value, params["state"])
|
||
redirect(conn, external: uri)
|
||
else
|
||
{:is_approved, false} ->
|
||
render(conn, :error,
|
||
description:
|
||
"Ваш аккаунт ещё не был одобрен, подождите немного или свяжитесь с администратором"
|
||
)
|
||
|
||
{:is_enabled, false} ->
|
||
render(conn, :error, description: "Приложение отключено")
|
||
|
||
{:has_uri, false} ->
|
||
render(conn, :error, description: "Redirect URI не зарегистрирован или отсутствует")
|
||
end
|
||
end
|
||
|
||
def token(conn, params = %{"code" => code_value, "redirect_uri" => redirect_uri}) do
|
||
{:ok, client_id, client_secret} = get_client_info(conn, params)
|
||
|
||
# Check that code is still valid and redirect uri has not been altered.
|
||
%OIDCCode{redirect_uri: ^redirect_uri} = code = SSO.get_oidc_code!(code_value)
|
||
|
||
# Check that client provided a valid secret for an active OIDC app.
|
||
%OIDCApp{enabled: true, client_id: ^client_id} =
|
||
oidc_app = SSO.get_oidc_app_by_secret!(client_secret)
|
||
|
||
# Check that OIDC app is referenced by provided code.
|
||
^client_id = code.oidc_app.client_id
|
||
|
||
# Delete the code.
|
||
SSO.delete_oidc_code(code)
|
||
|
||
{access_token, refresh_token} = Accounts.generate_oauth_tokens(code.user)
|
||
|
||
id_token = IDToken.build_id_token(code.user, oidc_app.client_id)
|
||
{:ok, signed_id_token} = Token.sign(id_token, client_secret)
|
||
|
||
render(conn, :token,
|
||
access_token: Base.url_encode64(access_token),
|
||
refresh_token: Base.url_encode64(refresh_token),
|
||
id_token: signed_id_token
|
||
)
|
||
end
|
||
|
||
def openid_discovery(conn, _params) do
|
||
render(conn, :openid_discovery)
|
||
end
|
||
|
||
@doc """
|
||
Extract client id and client secret from request parameters or headers.
|
||
Returns {:ok, "client_id", "client_secret"} on success.
|
||
"""
|
||
def get_client_info(_conn, %{"client_id" => client_id, "client_secret" => client_secret}) do
|
||
{:ok, client_id, client_secret}
|
||
end
|
||
|
||
def get_client_info(conn, _params) do
|
||
with [header] <- Plug.Conn.get_req_header(conn, "authorization"),
|
||
"Basic " <> b64 <- header,
|
||
{:ok, keys} <- Base.decode64(b64),
|
||
[client_id, client_secret] <- String.split(keys, ":") do
|
||
{:ok, client_id, client_secret}
|
||
else
|
||
_ -> {:error, "Invalid Authorization header"}
|
||
end
|
||
end
|
||
|
||
defp build_redirect_uri(redirect_uri, code, state) do
|
||
parsed_uri = URI.parse(redirect_uri)
|
||
|
||
query =
|
||
build_query_params(code, state)
|
||
|> URI.encode_query()
|
||
|
||
%{parsed_uri | query: query}
|
||
|> URI.to_string()
|
||
end
|
||
|
||
defp build_query_params(code, state) do
|
||
params = %{"code" => code}
|
||
|
||
if state do
|
||
Map.put(params, "state", state)
|
||
else
|
||
params
|
||
end
|
||
end
|
||
end
|