Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
maennchen committed Sep 25, 2024
1 parent 35ce3ad commit 27f4e62
Show file tree
Hide file tree
Showing 3 changed files with 415 additions and 1 deletion.
364 changes: 364 additions & 0 deletions lib/mix/tasks/oidcc_plug.gen.controller.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,364 @@
defmodule Mix.Tasks.OidccPlug.Gen.Controller do
@example "mix oidcc.gen.controller --name MyApp.AuthController --provider MyApp.OpenIDProvider --base-url /auth --issuer https://account.google.com --client-id client-id --client-secret client-secret"

@shortdoc "Generate an auth controller for your OpenID provider"
if !Code.ensure_loaded?(Igniter) do
@shortdoc "#{@shortdoc} | Install `igniter` to use"
end

@moduledoc """
#{@shortdoc}
Generates an auth controller that starts the OpenID Connect flow and handles
the result. Additionally, it will add the routes to your router.
## Example
```bash
#{@example}
```
## Options
* `--name` or `-n` - Name of the controller
* `--provider` or `-p` - Name of the OpenID Provider
* `--base-url` or `-b` - Base URL for the controller
* `--issuer` or `-i` - Issuer URL of the OpenID Provider
* `--client-id` - Client ID for the OpenID Provider
* `--client-secret` - Client Secret for the OpenID Provider
"""

if Code.ensure_loaded?(Igniter) do
use Igniter.Mix.Task

alias Igniter.Code.Module
alias Igniter.Libs.Phoenix
alias Igniter.Project.Config
alias Igniter.Project.IgniterConfig

def info(_argv, _composing_task) do

Check warning on line 39 in lib/mix/tasks/oidcc_plug.gen.controller.ex

View workflow job for this annotation

GitHub Actions / Test / mix credo

Functions should have a @SPEC type specification.
%Igniter.Mix.Task.Info{
# dependencies to add
adds_deps: [],
# dependencies to add and call their associated installers, if they exist
installs: [],
# An example invocation
example: @example,
# Accept additional arguments that are not in your schema
# Does not guarantee that, when composed, the only options you get are the ones you define
extra_args?: false,
# A list of environments that this should be installed in, only relevant if this is an installer.
only: nil,
# a list of positional arguments, i.e `[:file]`
positional: [],
# Other tasks your task composes using `Igniter.compose_task`, passing in the CLI argv
# This ensures your option schema includes options from nested tasks
composes: [],
# `OptionParser` schema
schema: [
name: :string,
provider: :string,
base_url: :string,
issuer: :string,
client_id: :string,
client_secret: :string
],
# CLI aliases
aliases: [n: :name, p: :provider, b: :base_url, i: :issuer]
}
end

@impl Igniter.Mix.Task
def igniter(igniter, argv) do
# extract positional arguments according to `positional` above
{_arguments, argv} = positional_args!(argv)
# extract options according to `schema` and `aliases` above
options = setup_options(argv, igniter)

# Do your work here and return an updated igniter
igniter
|> IgniterConfig.setup()
# TODO: Add Igniter ignore web folder
|> setup_provider(options)
|> setup_config(options)
|> generate_controller(options)
|> add_routes(options)
end

defp setup_options(argv, igniter) do
argv
|> options!()
|> Keyword.update(
:name,
Phoenix.web_module_name(igniter, "AuthController"),
&Module.parse/1
)
|> Keyword.update(
:provider,
Module.module_name(igniter, "OpenIDProvider"),
&Module.parse/1
)
|> Keyword.put_new(:base_url, "/auth")
|> Keyword.put(:app_name, Igniter.Project.Application.app_name(igniter))
end

defp setup_provider(igniter, options) do
Igniter.compose_task(igniter, "oidcc.gen.provider_configuration_worker", [
"--name",
inspect(options[:provider]),
"--issuer",
options[:issuer],
"--client-id",
options[:client_id],
"--client-secret",
options[:client_secret]
])
end

defp setup_config(igniter, options) do
env_prefix =
options[:provider] |> Macro.underscore() |> String.upcase() |> String.replace("/", "_")

client_id_config =
case Keyword.fetch(options, :issuer) do
{:ok, issuer} ->
quote do
System.get_env(unquote("#{env_prefix}_CLIENT_ID"), unquote(issuer))
end

:error ->
quote do
System.fetch_env!(unquote("#{env_prefix}_CLIENT_ID"))
end
end

client_secret_config =
case Keyword.fetch(options, :issuer) do
{:ok, issuer} ->
quote do
System.get_env(unquote("#{env_prefix}_CLIENT_SECRET"), unquote(issuer))
end

:error ->
quote do
System.fetch_env!(unquote("#{env_prefix}_CLIENT_SECRET"))
end
end

config =
quote do
[client_id: unquote(client_id_config), client_secret: unquote(client_secret_config)]
end

igniter
|> Config.configure_new(
"config.exs",
options[:app_name],
[options[:name], :provider],
options[:provider]
)
|> Config.configure_new(
"runtime.exs",
options[:app_name],
[options[:name]],
{:code, config}
)
end

defp generate_controller(igniter, options) do
web_module = Phoenix.web_module(igniter)

html_module_name =
options[:name]
|> inspect()
|> String.trim_trailing("Controller")
|> Kernel.<>("HTML")
|> then(&Module.module_name(igniter, &1))

html_path =
html_module_name |> inspect() |> String.split(".") |> List.last() |> Macro.underscore()

html_template_path =
Path.join([
igniter |> Igniter.Project.Module.proper_location(web_module) |> Path.rootname(".ex"),
"controllers",
html_path
])

page_html_template_path =
Path.join([
igniter |> Igniter.Project.Module.proper_location(web_module) |> Path.rootname(".ex"),
"controllers",
"page_html"
])

igniter
|> Module.create_module(
options[:name],
Sourceror.to_string(
quote do
defmodule unquote(options[:name]) do
use unquote(web_module), :controller

plug(
Oidcc.Plug.Authorize,
[
provider:
Application.compile_env(unquote(options[:app_name]), [__MODULE__, :provider]),
client_id: &__MODULE__.client_id/0,
client_secret: &__MODULE__.client_secret/0,
redirect_uri: &__MODULE__.callback_uri/0
]
when action in [:authorize]
)

plug(
Oidcc.Plug.AuthorizationCallback,
[
provider:
Application.compile_env(unquote(options[:app_name]), [__MODULE__, :provider]),
client_id: &__MODULE__.client_id/0,
client_secret: &__MODULE__.client_secret/0,
redirect_uri: &__MODULE__.callback_uri/0
]
when action in [:callback]
)

def authorize(conn, _params), do: conn

def callback(
%Plug.Conn{
private: %{Oidcc.Plug.AuthorizationCallback => {:ok, {_token, userinfo}}}
} =
conn,
params
) do
conn
|> put_session("oidcc_claims", userinfo)
|> redirect(
to:
case params[:state] do
nil -> "/"
state -> state
end
)
end

def callback(
%Plug.Conn{private: %{Oidcc.Plug.AuthorizationCallback => {:error, reason}}} =
conn,
_params
) do
conn
|> put_status(400)
|> render(:error, reason: reason)
end

@doc false
def client_id,
do: Application.fetch_env!(unquote(options[:app_name]), __MODULE__)[:client_id]

@doc false
def client_secret,
do:
Application.fetch_env!(unquote(options[:app_name]), __MODULE__)[:client_secret]

@doc false
def callback_uri,
do:
url(
unquote(
{:sigil_p, [delimiter: "\""],
[{:<<>>, [], ["#{options[:base_url]}/callback"]}, []]}
)
)
end
end
)
)
|> Module.create_module(
html_module_name,
Sourceror.to_string(
quote do
defmodule unquote(html_module_name) do
use unquote(web_module), :html

embed_templates(unquote("#{html_path}/*"))
end
end
)
)
|> Igniter.create_new_file(Path.join(html_template_path, "error.html.heex"), """
<p>error:</p>
<pre><%= inspect(@reason, pretty: true) %></pre>
""")
|> Igniter.update_file(Path.join(page_html_template_path, "home.html.heex"), fn current ->
Rewrite.Source.update(current, :content, """
#{Rewrite.Source.get(current, :content)}
<div class="px-4 py-10 sm:px-6 sm:py-28 lg:px-8 xl:px-28 xl:py-32">
<div class="mx-auto max-w-xl lg:mx-0">
<div class="group -mx-2 -my-0.5 inline-flex items-center gap-3 px-2 py-0.5">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="h-4 w-4">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z" />
</svg>
<%= case Plug.Conn.get_session(@conn, "oidcc_claims") do %>
<% nil -> %>
<a href={ ~p"#{options[:base_url]}/authorize" }>
Log In
</a>
<% %{"sub" => sub} -> %>
<span class="text-base text-zinc-600">Logged in as <%= sub %></span>
<% end %>
</div>
</div>
</div>
""")
end)
end

defp add_routes(igniter, options) do
case Phoenix.select_router(igniter) do
{igniter, nil} ->
Igniter.add_warning(igniter, """
No Phoenix router found, skipping Route installation.
See the Getting Started guide for instructions on installing AshJsonApi with `plug`.
If you have yet to set up Phoenix, you'll have to do that manually and then rerun this installer.
""")

{igniter, router} ->
Igniter.Libs.Phoenix.add_scope(

Check warning on line 333 in lib/mix/tasks/oidcc_plug.gen.controller.ex

View workflow job for this annotation

GitHub Actions / Test / mix credo

Nested modules could be aliased at the top of the invoking module.
igniter,
options[:base_url],
"""
pipe_through [:browser]
get "/authorize", #{inspect(options[:name])}, :authorize
get "/callback", #{inspect(options[:name])}, :callback
post "/callback", #{inspect(options[:name])}, :callback
""",
router: router,
arg2: Phoenix.web_module(igniter)
)
end
end
else
use Mix.Task

@impl Mix.Task
def run(_argv) do
Mix.shell().error("""
The task 'oidcc.gen.controller' requires igniter to be run.
Please install igniter and try again.
For more information, see: https://hexdocs.pm/igniter
""")

exit({:shutdown, 1})
end
end
end
6 changes: 5 additions & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,12 @@ defmodule Oidcc.Plug.MixProject do
# Run "mix help deps" to learn about dependencies.
defp deps do
[
{:oidcc, "~> 3.2.0"},
# TODO: Switch back to released version
# "~> 3.2.0"},
{:oidcc, github: "erlef/oidcc", branch: "igniter"},
{:plug, "~> 1.14"},
{:igniter, "~> 0.3.45", optional: true},
{:phoenix, "~> 1.7", only: [:dev, :test]},
{:ex_doc, "~> 0.29", only: :dev, runtime: false},
{:excoveralls, "~> 0.18.1", only: :test, runtime: false},
{:dialyxir, "~> 1.4", only: :dev, runtime: false},
Expand Down
Loading

0 comments on commit 27f4e62

Please sign in to comment.