defmodule ComfycampWeb.OauthController do use ComfycampWeb, :controller alias Comfycamp.Accounts alias Comfycamp.SSO alias Comfycamp.SSO.OIDCApp 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, nonce: params["nonce"] }) 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 with {:client_info, {:ok, client_id, client_secret}} <- {:client_info, get_client_info(conn, params)}, {:code, code} <- {:code, SSO.get_oidc_code!(code_value)}, {:uri, ^redirect_uri} <- {:uri, code.redirect_uri}, {:app, oidc_app = %OIDCApp{enabled: true, client_id: ^client_id}} <- {:app, SSO.get_oidc_app_by_secret!(client_secret)}, {:code_ref, ^client_id} <- {:code_ref, code.oidc_app.client_id} do 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, code.nonce) {: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 ) else {:client_info, _} -> render(conn, :error, description: "Нет client id или client secret") {:code, _} -> render(conn, :error, description: "Не удалось найти временный код") {:uri, _} -> render(conn, :error, description: "Redirect URI не совпадает с изначальным значением") {:app, _} -> render(conn, :error, description: "Приложение не найдено или отключено") {:code_ref, _} -> render(conn, :error, description: "Временный код выдан для другого приложения") end end def openid_discovery(conn, _params) do render(conn, :openid_discovery) end def user_info(conn, _params) do render(conn, :user_info, user: conn.assigns.oauth_user) 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