Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Igniter Setup #23

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading