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, %{ "code" => code_value, "redirect_uri" => redirect_uri, "client_id" => client_id, "client_secret" => client_secret }) do # 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 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