├── test ├── support │ ├── lib │ │ ├── dummy_web │ │ │ ├── templates │ │ │ │ ├── phoenix_oauth2_provider │ │ │ │ │ └── application │ │ │ │ │ │ └── index.html.eex │ │ │ │ └── layout │ │ │ │ │ └── app.html.eex │ │ │ ├── views │ │ │ │ ├── layout_view.ex │ │ │ │ ├── phoenix_oauth2_provider │ │ │ │ │ └── application_view.ex │ │ │ │ └── error_view.ex │ │ │ ├── router.ex │ │ │ └── endpoint.ex │ │ ├── dummy │ │ │ ├── repo.ex │ │ │ ├── user.ex │ │ │ ├── oauth_applications │ │ │ │ └── oauth_application.ex │ │ │ ├── oauth_access_grants │ │ │ │ └── oauth_access_grant.ex │ │ │ └── oauth_access_tokens │ │ │ │ └── oauth_access_token.ex │ │ └── dummy_web.ex │ ├── priv │ │ └── migrations │ │ │ ├── 2_create_oauth_tables.exs │ │ │ └── 1_create_users.exs │ ├── mix │ │ └── test_case.ex │ ├── conn_case.ex │ └── fixtures.ex ├── phoenix_oauth2_provider_test.exs ├── test_helper.exs ├── phoenix_oauth2_provider │ ├── controller_test.exs │ └── controllers │ │ ├── authorized_application_controller_test.exs │ │ ├── application_controller_test.exs │ │ ├── authorization_controller_test.exs │ │ └── token_controller_test.exs └── mix │ └── tasks │ └── phoenix_oauth2_provider.gen.templates_test.exs ├── config ├── config.exs └── test.exs ├── .gitignore ├── .travis.yml ├── lib ├── phoenix_oauth2_provider.ex ├── phoenix_oauth2_provider │ ├── views │ │ ├── authorized_application_view.ex │ │ ├── authorization_view.ex │ │ └── application_view.ex │ ├── controllers │ │ ├── token_controller.ex │ │ ├── authorized_application_controller.ex │ │ ├── authorization_controller.ex │ │ └── application_controller.ex │ ├── config.ex │ ├── view.ex │ ├── controller.ex │ └── router.ex └── mix │ ├── tasks │ └── phoenix_oauth2_provider.gen.templates.ex │ └── phoenix_oauth2_provider │ └── template.ex ├── LICENSE ├── mix.exs ├── CHANGELOG.md ├── README.md └── mix.lock /test/support/lib/dummy_web/templates/phoenix_oauth2_provider/application/index.html.eex: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | if Mix.env == :test do 4 | import_config "test.exs" 5 | end 6 | -------------------------------------------------------------------------------- /test/support/lib/dummy_web/templates/layout/app.html.eex: -------------------------------------------------------------------------------- 1 | <%= render @view_module, @view_template, assigns %> -------------------------------------------------------------------------------- /test/support/lib/dummy_web/views/layout_view.ex: -------------------------------------------------------------------------------- 1 | defmodule DummyWeb.LayoutView do 2 | use DummyWeb, :view 3 | end 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /cover 3 | /deps 4 | /doc 5 | /.fetch 6 | erl_crash.dump 7 | *.ez 8 | /tmp 9 | .DS_Store 10 | /.elixir_ls 11 | -------------------------------------------------------------------------------- /test/phoenix_oauth2_provider_test.exs: -------------------------------------------------------------------------------- 1 | defmodule PhoenixOauth2ProviderTest do 2 | use PhoenixOauth2Provider.ConnCase 3 | doctest PhoenixOauth2Provider 4 | end 5 | -------------------------------------------------------------------------------- /test/support/lib/dummy_web/views/phoenix_oauth2_provider/application_view.ex: -------------------------------------------------------------------------------- 1 | defmodule DummyWeb.PhoenixOauth2Provider.ApplicationView do 2 | use DummyWeb, :view 3 | end 4 | -------------------------------------------------------------------------------- /test/support/lib/dummy/repo.ex: -------------------------------------------------------------------------------- 1 | defmodule Dummy.Repo do 2 | use Ecto.Repo, otp_app: :phoenix_oauth2_provider, adapter: Ecto.Adapters.Postgres 3 | 4 | def log(_cmd), do: nil 5 | end 6 | -------------------------------------------------------------------------------- /test/support/lib/dummy_web/views/error_view.ex: -------------------------------------------------------------------------------- 1 | defmodule DummyWeb.ErrorView do 2 | def render("500.html", _changeset), do: "500.html" 3 | def render("400.html", _changeset), do: "400.html" 4 | def render("404.html", _changeset), do: "404.html" 5 | end 6 | -------------------------------------------------------------------------------- /test/support/priv/migrations/2_create_oauth_tables.exs: -------------------------------------------------------------------------------- 1 | require Mix.ExOauth2Provider.Migration 2 | 3 | binary_id = if System.get_env("UUID"), do: true, else: false 4 | "CreateOauthTables" 5 | |> Mix.ExOauth2Provider.Migration.gen("oauth", %{repo: Dummy.Repo, binary_id: binary_id}) 6 | |> Code.eval_string() 7 | -------------------------------------------------------------------------------- /test/support/lib/dummy_web.ex: -------------------------------------------------------------------------------- 1 | defmodule DummyWeb do 2 | @moduledoc false 3 | 4 | def view do 5 | quote do 6 | use Phoenix.View, 7 | root: "test/support/lib/dummy_web/templates", 8 | namespace: DummyWeb 9 | end 10 | end 11 | 12 | defmacro __using__(which) when is_atom(which) do 13 | apply(__MODULE__, which, []) 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: elixir 2 | elixir: 1.8 3 | otp_release: 22.0 4 | services: 5 | postgresql 6 | jobs: 7 | include: 8 | - stage: test 9 | elixir: 1.8 10 | otp_release: 21.0 11 | script: &test_scripts 12 | - mix test 13 | - MIX_ENV=test mix credo 14 | - stage: test 15 | script: *test_scripts 16 | env: 17 | - UUID=true 18 | -------------------------------------------------------------------------------- /test/support/priv/migrations/1_create_users.exs: -------------------------------------------------------------------------------- 1 | defmodule Dummy.Repo.Migrations.CreateUser do 2 | use Ecto.Migration 3 | 4 | def change do 5 | create table(:users, primary_key: is_nil(System.get_env("UUID"))) do 6 | if System.get_env("UUID") do 7 | add :id, :binary_id, primary_key: true 8 | end 9 | add :email, :string 10 | 11 | timestamps() 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /test/support/lib/dummy/user.ex: -------------------------------------------------------------------------------- 1 | defmodule Dummy.Users.User do 2 | @moduledoc false 3 | use Ecto.Schema 4 | 5 | if System.get_env("UUID") do 6 | @primary_key {:id, :binary_id, autogenerate: true} 7 | @foreign_key_type :binary_id 8 | end 9 | 10 | schema "users" do 11 | field :email, :string 12 | has_many :tokens, Dummy.OauthAccessTokens.OauthAccessToken, foreign_key: :resource_owner_id 13 | 14 | timestamps() 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /test/support/mix/test_case.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixOauth2Provider.Mix.TestCase do 2 | @moduledoc false 3 | use ExUnit.CaseTemplate 4 | 5 | setup_all do 6 | clear_tmp_files() 7 | 8 | :ok 9 | end 10 | 11 | setup _context do 12 | current_shell = Mix.shell() 13 | 14 | on_exit fn -> 15 | Mix.shell(current_shell) 16 | end 17 | 18 | Mix.shell(Mix.Shell.Process) 19 | 20 | :ok 21 | end 22 | 23 | defp clear_tmp_files, do: File.rm_rf!("tmp") 24 | end 25 | -------------------------------------------------------------------------------- /test/support/lib/dummy/oauth_applications/oauth_application.ex: -------------------------------------------------------------------------------- 1 | defmodule Dummy.OauthApplications.OauthApplication do 2 | @moduledoc false 3 | 4 | use Ecto.Schema 5 | use ExOauth2Provider.Applications.Application, otp_app: :phoenix_oauth2_provider 6 | 7 | if System.get_env("UUID") do 8 | @primary_key {:id, :binary_id, autogenerate: true} 9 | @foreign_key_type :binary_id 10 | end 11 | 12 | schema "oauth_applications" do 13 | application_fields() 14 | timestamps() 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /test/support/lib/dummy/oauth_access_grants/oauth_access_grant.ex: -------------------------------------------------------------------------------- 1 | defmodule Dummy.OauthAccessGrants.OauthAccessGrant do 2 | @moduledoc false 3 | 4 | use Ecto.Schema 5 | use ExOauth2Provider.AccessGrants.AccessGrant, otp_app: :phoenix_oauth2_provider 6 | 7 | if System.get_env("UUID") do 8 | @primary_key {:id, :binary_id, autogenerate: true} 9 | @foreign_key_type :binary_id 10 | end 11 | 12 | schema "oauth_access_grants" do 13 | access_grant_fields() 14 | timestamps() 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /test/support/lib/dummy/oauth_access_tokens/oauth_access_token.ex: -------------------------------------------------------------------------------- 1 | defmodule Dummy.OauthAccessTokens.OauthAccessToken do 2 | @moduledoc false 3 | 4 | use Ecto.Schema 5 | use ExOauth2Provider.AccessTokens.AccessToken, otp_app: :phoenix_oauth2_provider 6 | 7 | if System.get_env("UUID") do 8 | @primary_key {:id, :binary_id, autogenerate: true} 9 | @foreign_key_type :binary_id 10 | end 11 | 12 | schema "oauth_access_tokens" do 13 | access_token_fields() 14 | timestamps() 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/phoenix_oauth2_provider.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixOauth2Provider do 2 | @moduledoc """ 3 | A module that provides OAuth 2 server capabilities for Phoenix applications. 4 | 5 | ## Configuration 6 | config :phoenix_oauth2_provider, PhoenixOauth2Provider, 7 | current_resource_owner: :current_user, 8 | module: MyApp, 9 | router: MyApp.Router 10 | 11 | You can find more config options in the 12 | [ex_oauth2_provider](https://github.com/danschultzer/ex_oauth2_provider) 13 | library. 14 | """ 15 | end 16 | -------------------------------------------------------------------------------- /test/support/lib/dummy_web/router.ex: -------------------------------------------------------------------------------- 1 | defmodule DummyWeb.Router do 2 | use Phoenix.Router 3 | use PhoenixOauth2Provider.Router, otp_app: :phoenix_oauth2_provider 4 | 5 | pipeline :browser do 6 | plug :accepts, ["html"] 7 | plug :fetch_session 8 | plug :fetch_flash 9 | plug :protect_from_forgery 10 | plug :put_secure_browser_headers 11 | end 12 | 13 | scope "/" do 14 | oauth_api_routes() 15 | end 16 | 17 | scope "/" do 18 | pipe_through :browser 19 | 20 | oauth_routes() 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | Logger.configure(level: :warn) 2 | 3 | ExUnit.start() 4 | 5 | # Ensure that symlink to custom ecto priv directory exists 6 | source = Dummy.Repo.config()[:priv] 7 | target = Application.app_dir(:phoenix_oauth2_provider, source) 8 | File.rm_rf(target) 9 | File.mkdir_p(target) 10 | File.rmdir(target) 11 | :ok = :file.make_symlink(Path.expand(source), target) 12 | 13 | # Set up database 14 | Mix.Task.run("ecto.drop", ~w(--quiet -r Dummy.Repo)) 15 | Mix.Task.run("ecto.create", ~w(--quiet -r Dummy.Repo)) 16 | Mix.Task.run("ecto.migrate", ~w(-r Dummy.Repo)) 17 | 18 | {:ok, _pid} = DummyWeb.Endpoint.start_link() 19 | {:ok, _pid} = Dummy.Repo.start_link() 20 | 21 | Ecto.Adapters.SQL.Sandbox.mode(Dummy.Repo, :manual) 22 | -------------------------------------------------------------------------------- /config/test.exs: -------------------------------------------------------------------------------- 1 | use Mix.Config 2 | 3 | config :phoenix, :json_library, Jason 4 | 5 | config :phoenix_oauth2_provider, namespace: Dummy 6 | 7 | config :phoenix_oauth2_provider, DummyWeb.Endpoint, 8 | secret_key_base: "1lJGFCaor+gPGc21GCvn+NE0WDOA5ujAMeZoy7oC5un7NPUXDir8LAE+Iba5bpGH", 9 | render_errors: [view: DummyWeb.ErrorView, accepts: ~w(html json)] 10 | 11 | config :phoenix_oauth2_provider, Dummy.Repo, 12 | database: "phoenix_oauth2_provider_test", 13 | pool: Ecto.Adapters.SQL.Sandbox, 14 | priv: "test/support/priv" 15 | 16 | config :phoenix_oauth2_provider, ExOauth2Provider, 17 | repo: Dummy.Repo, 18 | resource_owner: Dummy.Users.User, 19 | scopes: ~w(read write), 20 | use_refresh_token: true 21 | 22 | config :phoenix_oauth2_provider, PhoenixOauth2Provider, 23 | current_resource_owner: :current_test_user 24 | -------------------------------------------------------------------------------- /test/support/lib/dummy_web/endpoint.ex: -------------------------------------------------------------------------------- 1 | defmodule DummyWeb.Endpoint do 2 | use Phoenix.Endpoint, otp_app: :phoenix_oauth2_provider 3 | 4 | # Serve at "/" the static files from "priv/static" directory. 5 | # 6 | # You should set gzip to true if you are running phoenix.digest 7 | # when deploying your static files in production. 8 | plug Plug.Static, 9 | at: "/", from: :ex_admin, gzip: false, 10 | only: ~w(css fonts images js favicon.ico robots.txt) 11 | 12 | plug Plug.RequestId 13 | plug Plug.Logger 14 | 15 | plug Plug.Parsers, 16 | parsers: [:urlencoded, :multipart, :json], 17 | pass: ["*/*"], 18 | json_decoder: Jason 19 | 20 | plug Plug.MethodOverride 21 | plug Plug.Head 22 | 23 | plug Plug.Session, 24 | store: :cookie, 25 | key: "_binaryid_key", 26 | signing_salt: "JFbk5iZ6" 27 | 28 | plug DummyWeb.Router 29 | end 30 | -------------------------------------------------------------------------------- /lib/phoenix_oauth2_provider/views/authorized_application_view.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixOauth2Provider.AuthorizedApplicationView do 2 | use PhoenixOauth2Provider.View 3 | 4 | template "index.html", 5 | """ 6 |
| Application | 12 |Created At | 13 |14 | |
|---|---|---|
| <%%= application.name %> | 20 |<%%= application.inserted_at %> | 21 | 22 |23 | <%%= link "Delete", to: Routes.oauth_authorized_application_path(@conn, :delete, application), method: :delete, data: [confirm: "Are you sure?"], class: "btn btn-danger btn-xs" %> 24 | | 25 |
<%%= @error[:error_description] %>
10 |This application will be able to:
19 |<%%= @code %>
51 | """
52 | end
53 |
--------------------------------------------------------------------------------
/lib/phoenix_oauth2_provider/controllers/authorization_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule PhoenixOauth2Provider.AuthorizationController do
2 | @moduledoc false
3 | use PhoenixOauth2Provider.Controller
4 |
5 | alias ExOauth2Provider.Authorization
6 | alias Plug.Conn
7 |
8 | @spec new(Conn.t(), map(), map(), keyword()) :: Conn.t()
9 | def new(conn, params, resource_owner, config) do
10 | resource_owner
11 | |> Authorization.preauthorize(params, config)
12 | |> case do
13 | {:ok, client, scopes} ->
14 | render(conn, "new.html", params: params, client: client, scopes: scopes)
15 |
16 | {:native_redirect, %{code: code}} ->
17 | redirect(conn, to: Routes.oauth_authorization_path(conn, :show, code))
18 |
19 | {:redirect, redirect_uri} ->
20 | redirect(conn, external: redirect_uri)
21 |
22 | {:error, error, status} ->
23 | conn
24 | |> put_status(status)
25 | |> render("error.html", error: error)
26 | end
27 | end
28 |
29 | @spec create(Conn.t(), map(), map(), keyword()) :: Conn.t()
30 | def create(conn, params, resource_owner, config) do
31 | resource_owner
32 | |> Authorization.authorize(params, config)
33 | |> redirect_or_render(conn)
34 | end
35 |
36 | @spec delete(Conn.t(), map(), map(), keyword()) :: Conn.t()
37 | def delete(conn, params, resource_owner, config) do
38 | resource_owner
39 | |> Authorization.deny(params, config)
40 | |> redirect_or_render(conn)
41 | end
42 |
43 | @spec show(Conn.t(), map(), map(), keyword()) :: Conn.t()
44 | def show(conn, %{"code" => code}, _resource_owner, _config) do
45 | render(conn, "show.html", code: code)
46 | end
47 |
48 | defp redirect_or_render({:redirect, redirect_uri}, conn) do
49 | redirect(conn, external: redirect_uri)
50 | end
51 | defp redirect_or_render({:native_redirect, payload}, conn) do
52 | json(conn, payload)
53 | end
54 | defp redirect_or_render({:error, error, status}, conn) do
55 | conn
56 | |> put_status(status)
57 | |> json(error)
58 | end
59 | end
60 |
--------------------------------------------------------------------------------
/mix.exs:
--------------------------------------------------------------------------------
1 | defmodule PhoenixOauth2Provider.Mixfile do
2 | use Mix.Project
3 |
4 | @version "0.5.1"
5 |
6 | def project do
7 | [
8 | app: :phoenix_oauth2_provider,
9 | version: @version,
10 | elixir: "~> 1.8",
11 | elixirc_paths: elixirc_paths(Mix.env),
12 | start_permanent: Mix.env == :prod,
13 | compilers: [:phoenix] ++ Mix.compilers,
14 | deps: deps(),
15 |
16 | # Hex
17 | description: "The fastest way to set up OAuth 2.0 server in your Phoenix app",
18 | package: package(),
19 |
20 | # Docs
21 | name: "PhoenixOauth2Provider",
22 | docs: docs()
23 | ]
24 | end
25 |
26 | def application do
27 | [extra_applications: extra_applications(Mix.env)]
28 | end
29 |
30 | defp extra_applications(:test), do: [:ecto, :logger]
31 | defp extra_applications(_), do: [:logger]
32 |
33 | defp elixirc_paths(:test), do: ["lib", "test/support"]
34 | defp elixirc_paths(_), do: ["lib"]
35 |
36 | defp deps do
37 | [
38 | {:ex_oauth2_provider, "~> 0.5.1"},
39 | {:phoenix, "~> 1.4"},
40 | {:phoenix_html, "~> 2.0"},
41 |
42 | {:phoenix_ecto, "~> 4.0.0", only: [:test, :dev]},
43 | {:credo, "~> 1.1.0", only: [:dev, :test]},
44 | {:jason, "~> 1.0", only: [:dev, :test]},
45 |
46 | {:ex_doc, ">= 0.0.0", only: :dev},
47 |
48 | {:ecto_sql, "~> 3.0.0", only: :test},
49 | {:plug_cowboy, "~> 2.0", only: :test},
50 | {:postgrex, "~> 0.14.0", only: :test}
51 | ]
52 | end
53 |
54 | defp package do
55 | [
56 | maintainers: ["Dan Shultzer", "Benjamin Schultzer"],
57 | licenses: ["MIT"],
58 | links: %{github: "https://github.com/danschultzer/phoenix_oauth2_provider"},
59 | files: ~w(lib LICENSE mix.exs README.md)
60 | ]
61 | end
62 |
63 | defp docs do
64 | [
65 | source_ref: "v#{@version}", main: "PhoenixOauth2Provider",
66 | canonical: "http://hexdocs.pm/phoenix_oauth2_provider",
67 | source_url: "https://github.com/danschultzer/phoenix_oauth2_provider",
68 | extras: ["README.md"]
69 | ]
70 | end
71 | end
72 |
--------------------------------------------------------------------------------
/lib/mix/phoenix_oauth2_provider/template.ex:
--------------------------------------------------------------------------------
1 | defmodule Mix.PhoenixOauth2Provider.Template do
2 | @moduledoc false
3 |
4 | alias Mix.{ExOauth2Provider, Generator, Phoenix}
5 |
6 | @views ["application", "authorization", "authorized_application"]
7 | @view_template """
8 | defmodule <%= inspect view_module %> do
9 | use <%= inspect web_module %>, :view
10 | end
11 | """
12 |
13 | @spec create_view_and_template_files(binary()) :: map()
14 | def create_view_and_template_files(context_app) do
15 | web_module =
16 | context_app
17 | |> web_app()
18 | |> Macro.camelize()
19 | |> List.wrap()
20 | |> Module.concat()
21 | web_path = web_path(context_app)
22 |
23 | for name <- @views do
24 | create_view_file(web_module, web_path, name)
25 | create_template_files(web_path, name)
26 | end
27 | end
28 |
29 | defp create_view_file(web_module, web_path, name) do
30 | view_name = "#{name}_view"
31 | dir = Path.join([web_path, "views", "phoenix_oauth2_provider"])
32 | path = Path.join(dir, "#{view_name}.ex")
33 | view_module = Module.concat([web_module, PhoenixOauth2Provider, Macro.camelize(view_name)])
34 | content = EEx.eval_string(@view_template, view_module: view_module, web_module: web_module)
35 |
36 | Generator.create_directory(dir)
37 | Generator.create_file(path, content)
38 | end
39 |
40 | defp create_template_files(web_path, name) do
41 | view_module = Module.concat([PhoenixOauth2Provider, Macro.camelize("#{name}_view")])
42 |
43 | for template <- view_module.templates() do
44 | content = view_module.html(template)
45 | dir = Path.join([web_path, "templates", "phoenix_oauth2_provider", name])
46 | path = Path.join(dir, "#{template}.eex")
47 |
48 | Generator.create_directory(dir)
49 | Generator.create_file(path, content)
50 | end
51 | end
52 |
53 | defp web_path(context_app), do: Phoenix.web_path(context_app)
54 |
55 | defp web_app(ctx_app) do
56 | this_app = ExOauth2Provider.otp_app()
57 |
58 | if ctx_app == this_app do
59 | "#{ctx_app}_web"
60 | else
61 | ctx_app
62 | end
63 | end
64 | end
65 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## v0.5.1 (2019-05-08)
4 |
5 | * Require ExOauth2Provider v0.5.1
6 | * Pass configuration to all ExOauth2Provider methods
7 | * Removed `ExOauth2Provider.Config.native_redirect_uri/1` call in templates in favor of using assigns
8 |
9 | ## v0.5.0 (2019-05-08)
10 |
11 | This is a full rewrite of the library, and are several breaking changes. You're encouraged to test your app well if you upgrade from 0.4.x.
12 |
13 | ### 1. ExOauth2Provider
14 |
15 | Read the [ExOauth2Provider](https://github.com/danschultzer/ex_oauth2_provider) CHANGELOG.md for upgrade instructions.
16 |
17 | ### 2. Configuration
18 |
19 | Configuration has been split up so now it should look like this (`:module` and `:current_resource_owner` should be moved to the separate configuration for PhoenixOauth2Provider):
20 |
21 | ```elixir
22 | config :my_app, ExOauth2Provider,
23 | repo: MyApp.Repo,
24 | resource_owner: MyApp.Users.User,
25 | # ...
26 |
27 | config :my_app, PhoenixOauth2Provider,
28 | current_resource_owner: :current_user,
29 | web_module: MyAppWeb
30 | ```
31 |
32 | ### 3. Routes
33 |
34 | Routes are now separated into api and non api routes. Remove the old `oauth_routes/1` routes, and update your `router.ex` to look like this instead:
35 |
36 | ```elixir
37 | defmodule MyAppWeb.Router do
38 | use MyAppWeb, :router
39 | use PhoenixOauth2Provider.Router
40 |
41 | # ...
42 |
43 | pipeline :protected do
44 | # Require user authentication
45 | end
46 |
47 | scope "/" do
48 | pipe_through :api
49 |
50 | oauth_api_routes()
51 | end
52 |
53 | scope "/" do
54 | pipe_through [:browser, :protected]
55 |
56 | oauth_routes()
57 | end
58 |
59 | # ...
60 | end
61 | ```
62 |
63 | Remember to remove the `:oauth_public` pipeline. The default `:api` pipeline will be used instead.
64 |
65 | ### 4. Templates and views
66 |
67 | Remove the old `module: MyApp` setting in your PhoenixOauth2Provider configuration, and instead set `web_module: MyAppWeb`.
68 |
69 | However, templates and views are no longer required to be generated so you can remove them and the `:web_module` configuration setting entirely if the default ones work for you.
70 |
71 | The easiest migration of templates and views are to just delete the folders (`lib/my_app_web/templates/{application, authorization, authorized_application}` and `lib/my_app_web/views/phoenix_oauth2_provider`), and then run `mix phoenix_oauth2_provider.gen.templates` to regenerate them. Then you can go into the templates and update them to use your old markup.
--------------------------------------------------------------------------------
/lib/phoenix_oauth2_provider/controller.ex:
--------------------------------------------------------------------------------
1 | defmodule PhoenixOauth2Provider.Controller do
2 | @moduledoc false
3 |
4 | alias PhoenixOauth2Provider.Config
5 | alias Plug.Conn
6 |
7 | @doc false
8 | defmacro __using__(type) do
9 | quote do
10 | use Phoenix.Controller
11 |
12 | alias PhoenixOauth2Provider.Router.Helpers, as: Routes
13 |
14 | plug :put_web_module_view, unquote(type)
15 |
16 | defdelegate put_web_module_view(conn, type), to: unquote(__MODULE__), as: :__put_web_module_view__
17 |
18 | def action(conn, _opts) do
19 | config = conn.private[:phoenix_oauth2_provider_config]
20 | params = unquote(__MODULE__).__action_params__(conn, config, unquote(type))
21 |
22 | apply(__MODULE__, action_name(conn), params)
23 | end
24 | end
25 | end
26 |
27 | @doc false
28 | def __put_web_module_view__(conn, :api), do: conn
29 | def __put_web_module_view__(conn, _type) do
30 | web_module =
31 | conn
32 | |> load_config()
33 | |> Config.web_module()
34 |
35 | conn
36 | |> put_layout(web_module)
37 | |> put_view(web_module)
38 | end
39 |
40 | defp put_layout(conn, nil) do
41 | ["Endpoint" | web_context] =
42 | conn
43 | |> Phoenix.Controller.endpoint_module()
44 | |> Module.split()
45 | |> Enum.reverse()
46 |
47 | web_module =
48 | web_context
49 | |> Enum.reverse()
50 | |> Module.concat()
51 |
52 | put_layout(conn, web_module)
53 | end
54 | defp put_layout(conn, web_module) do
55 | conn
56 | |> Phoenix.Controller.layout()
57 | |> case do
58 | {PhoenixOauth2Provider.LayoutView, template} ->
59 | view = Module.concat([web_module, LayoutView])
60 |
61 | Phoenix.Controller.put_layout(conn, {view, template})
62 |
63 | _layout ->
64 | conn
65 | end
66 | end
67 |
68 | defp put_view(conn, nil), do: conn
69 | defp put_view(%{private: %{phoenix_view: phoenix_view}} = conn, web_module) do
70 | view_module = Module.concat([web_module, phoenix_view])
71 |
72 | Phoenix.Controller.put_view(conn, view_module)
73 | end
74 |
75 | defp load_config(conn), do: Map.get(conn.private, :phoenix_oauth2_provider_config, [])
76 |
77 | @doc false
78 | def __action_params__(conn, config, :api), do: [conn, conn.params, config]
79 | def __action_params__(conn, config, _any), do: [conn, conn.params, current_resource_owner(conn, config), config]
80 |
81 | defp current_resource_owner(conn, config) do
82 | resource_owner_key = Config.current_resource_owner(config)
83 |
84 | case Map.get(conn.assigns, resource_owner_key) do
85 | nil -> raise "Resource owner was not found with :#{resource_owner_key} assigns"
86 | resource_owner -> resource_owner
87 | end
88 | end
89 |
90 | @spec routes(Conn.t()) :: module()
91 | def routes(conn) do
92 | Module.concat([conn.private[:phoenix_router], Helpers])
93 | end
94 | end
95 |
--------------------------------------------------------------------------------
/lib/phoenix_oauth2_provider/controllers/application_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule PhoenixOauth2Provider.ApplicationController do
2 | @moduledoc false
3 | use PhoenixOauth2Provider.Controller
4 |
5 | alias ExOauth2Provider.Applications
6 | alias Plug.Conn
7 |
8 | plug :assign_native_redirect_uri when action in [:new, :create, :edit, :update]
9 |
10 | @spec index(Conn.t(), map(), map(), keyword()) :: Conn.t()
11 | def index(conn, _params, resource_owner, config) do
12 | applications = Applications.get_applications_for(resource_owner, config)
13 |
14 | render(conn, "index.html", applications: applications)
15 | end
16 |
17 | @spec new(Conn.t(), map(), map(), keyword()) :: Conn.t()
18 | def new(conn, _params, _resource_owner, config) do
19 | changeset =
20 | ExOauth2Provider.Config.application(config)
21 | |> struct()
22 | |> Applications.change_application(%{}, config)
23 |
24 | render(conn, "new.html", changeset: changeset)
25 | end
26 |
27 | @spec create(Conn.t(), map(), map(), keyword()) :: Conn.t()
28 | def create(conn, %{"oauth_application" => application_params}, resource_owner, config) do
29 | resource_owner
30 | |> Applications.create_application(application_params, config)
31 | |> case do
32 | {:ok, application} ->
33 | conn
34 | |> put_flash(:info, "Application created successfully.")
35 | |> redirect(to: Routes.oauth_application_path(conn, :show, application))
36 |
37 | {:error, %Ecto.Changeset{} = changeset} ->
38 | render(conn, "new.html", changeset: changeset)
39 | end
40 | end
41 |
42 | @spec show(Conn.t(), map(), map(), keyword()) :: Conn.t()
43 | def show(conn, %{"uid" => uid}, resource_owner, config) do
44 | application = get_application_for!(resource_owner, uid, config)
45 |
46 | render(conn, "show.html", application: application)
47 | end
48 |
49 | @spec edit(Conn.t(), map(), map(), keyword()) :: Conn.t()
50 | def edit(conn, %{"uid" => uid}, resource_owner, config) do
51 | application = get_application_for!(resource_owner, uid, config)
52 | changeset = Applications.change_application(application, %{}, config)
53 |
54 | render(conn, "edit.html", changeset: changeset)
55 | end
56 |
57 | @spec update(Conn.t(), map(), map(), keyword()) :: Conn.t()
58 | def update(conn, %{"uid" => uid, "oauth_application" => application_params}, resource_owner, config) do
59 | application = get_application_for!(resource_owner, uid, config)
60 |
61 | case Applications.update_application(application, application_params, config) do
62 | {:ok, application} ->
63 | conn
64 | |> put_flash(:info, "Application updated successfully.")
65 | |> redirect(to: Routes.oauth_application_path(conn, :show, application))
66 |
67 | {:error, %Ecto.Changeset{} = changeset} ->
68 | render(conn, "edit.html", changeset: changeset)
69 | end
70 | end
71 |
72 | @spec delete(Conn.t(), map(), map(), keyword()) :: Conn.t()
73 | def delete(conn, %{"uid" => uid}, resource_owner, config) do
74 | {:ok, _application} =
75 | resource_owner
76 | |> get_application_for!(uid, config)
77 | |> Applications.delete_application(config)
78 |
79 | conn
80 | |> put_flash(:info, "Application deleted successfully.")
81 | |> redirect(to: Routes.oauth_application_path(conn, :index))
82 | end
83 |
84 | defp get_application_for!(resource_owner, uid, config) do
85 | Applications.get_application_for!(resource_owner, uid, config)
86 | end
87 |
88 | defp assign_native_redirect_uri(conn, _opts) do
89 | native_redirect_uri = ExOauth2Provider.Config.native_redirect_uri(conn.private[:phoenix_oauth2_provider_config])
90 |
91 | Conn.assign(conn, :native_redirect_uri, native_redirect_uri)
92 | end
93 | end
94 |
--------------------------------------------------------------------------------
/test/phoenix_oauth2_provider/controllers/application_controller_test.exs:
--------------------------------------------------------------------------------
1 | defmodule PhoenixOauth2Provider.ApplicationControllerTest do
2 | use PhoenixOauth2Provider.ConnCase
3 |
4 | alias PhoenixOauth2Provider.Test.Fixtures
5 | alias Plug.Conn
6 |
7 | @create_attrs %{name: "Example", redirect_uri: "https://example.com"}
8 | @update_attrs %{name: "some updated name"}
9 | @invalid_attrs %{name: nil}
10 |
11 | setup %{conn: conn} do
12 | user = Fixtures.user()
13 | conn = Conn.assign(conn, :current_test_user, user)
14 |
15 | {:ok, conn: conn, user: user}
16 | end
17 |
18 | test "index/2 lists all entries on index", %{conn: conn, user: user} do
19 | application1 = Fixtures.application(%{user: user, name: "Application 1"})
20 | application2 = Fixtures.application(%{user: Fixtures.user(), name: "Application 2"})
21 |
22 | conn = get conn, Routes.oauth_application_path(conn, :index)
23 | body = html_response(conn, 200)
24 |
25 | assert body =~ "Your applications"
26 | assert body =~ application1.name
27 | refute body =~ application2.name
28 | end
29 |
30 | test "new/2 renders form for new applications", %{conn: conn} do
31 | conn = get conn, Routes.oauth_application_path(conn, :new)
32 | assert html_response(conn, 200) =~ "New Application"
33 | end
34 |
35 | test "create/2 creates application and redirects to show when data is valid", %{conn: authed_conn} do
36 | conn = post authed_conn, Routes.oauth_application_path(authed_conn, :create), oauth_application: @create_attrs
37 |
38 | assert %{uid: uid} = redirected_params(conn)
39 | assert redirected_to(conn) == Routes.oauth_application_path(conn, :show, uid)
40 |
41 | conn = get authed_conn, Routes.oauth_application_path(authed_conn, :show, uid)
42 | assert html_response(conn, 200) =~ "Show Application"
43 | end
44 |
45 | test "create/2 does not create application and renders errors when data is invalid", %{conn: conn} do
46 | conn = post conn, Routes.oauth_application_path(conn, :create), oauth_application: @invalid_attrs
47 | assert html_response(conn, 200) =~ "New Application"
48 | end
49 |
50 | test "edit/2 renders form for editing chosen application", %{conn: conn, user: user} do
51 | application = Fixtures.application(%{user: user})
52 | conn = get conn, Routes.oauth_application_path(conn, :edit, application)
53 | assert html_response(conn, 200) =~ "Edit Application"
54 | end
55 |
56 | test "update/2 updates chosen application and redirects when data is valid", %{conn: authed_conn, user: user} do
57 | application = Fixtures.application(%{user: user})
58 | conn = put authed_conn, Routes.oauth_application_path(authed_conn, :update, application), oauth_application: @update_attrs
59 | assert redirected_to(conn) == Routes.oauth_application_path(conn, :show, application)
60 |
61 | conn = get authed_conn, Routes.oauth_application_path(authed_conn, :show, application)
62 | assert html_response(conn, 200) =~ @update_attrs.name
63 | end
64 |
65 | test "update/2 does not update chosen application and renders errors when data is invalid", %{conn: conn, user: user} do
66 | application = Fixtures.application(%{user: user})
67 | conn = put conn, Routes.oauth_application_path(conn, :update, application), oauth_application: @invalid_attrs
68 | assert html_response(conn, 200) =~ "Edit Application"
69 | end
70 |
71 | test "delete/2 deletes chosen application", %{conn: authed_conn, user: user} do
72 | application = Fixtures.application(%{user: user})
73 | conn = delete authed_conn, Routes.oauth_application_path(authed_conn, :delete, application)
74 | assert redirected_to(conn) == Routes.oauth_application_path(conn, :index)
75 |
76 | assert_error_sent 404, fn ->
77 | get authed_conn, Routes.oauth_application_path(authed_conn, :show, application)
78 | end
79 | end
80 | end
81 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # PhoenixOauth2Provider
2 |
3 | [](https://travis-ci.org/danschultzer/phoenix_oauth2_provider) [](https://hex.pm/packages/phoenix_oauth2_provider) [](https://hex.pm/packages/phoenix_oauth2_provider)
4 |
5 | Get an OAuth 2.0 provider running in your Phoenix app with schema modules and templates in just two minutes.
6 |
7 | ## Installation
8 |
9 | Add PhoenixOauth2Provider to your list of dependencies in `mix.exs`:
10 |
11 | ```elixir
12 | def deps do
13 | [
14 | # ...
15 | {:phoenix_oauth2_provider, "~> 0.5.1"}
16 | # ...
17 | ]
18 | end
19 | ```
20 |
21 | Run `mix deps.get` to install it.
22 |
23 | ## Getting started
24 |
25 | Install ExOauthProvider first:
26 |
27 | ```bash
28 | mix ex_oauth2_provider.install
29 | ```
30 |
31 | Follow the instructions to update `config/config.exs`.
32 |
33 | Set up routes:
34 |
35 | ```elixir
36 | defmodule MyAppWeb.Router do
37 | use MyAppWeb, :router
38 | use PhoenixOauth2Provider.Router, otp_app: :my_app
39 |
40 | # ...
41 |
42 | pipeline :protected do
43 | # Require user authentication
44 | end
45 |
46 | scope "/" do
47 | pipe_through :api
48 |
49 | oauth_api_routes()
50 | end
51 |
52 | scope "/" do
53 | pipe_through [:browser, :protected]
54 |
55 | oauth_routes()
56 | end
57 |
58 | # ...
59 | end
60 | ```
61 |
62 | That's it! The following OAuth 2.0 routes will now be available in your app:
63 |
64 | ```text
65 | oauth_authorize_path GET /oauth/authorize AuthorizationController :new
66 | oauth_authorize_path POST /oauth/authorize AuthorizationController :create
67 | oauth_authorize_path GET /oauth/authorize/:code AuthorizationController :show
68 | oauth_authorize_path DELETE /oauth/authorize AuthorizationController :delete
69 | oauth_token_path POST /oauth/token TokenController :create
70 | oauth_token_path POST /oauth/revoke TokenController :revoke
71 | ```
72 |
73 | Please read the [ExOauth2Provider](https://github.com/danschultzer/ex_oauth2_provider) documentation for further customization.
74 |
75 | ## Configuration
76 |
77 | ### Templates
78 |
79 | To generate views and templates run:
80 |
81 | ```bash
82 | mix phoenix_oauth2_provider.gen.templates
83 | ```
84 |
85 | Set up the PhoenixOauth2Provider configuration with `:web_module`:
86 |
87 | ```elixir
88 | config :my_app, PhoenixOauth2Provider,
89 | web_module: MyAppWeb
90 | ```
91 |
92 | ### Current resource owner
93 |
94 | Set up what key in the plug conn `assigns` that PhoenixOauth2Provider should use to fetch the current resource owner.
95 |
96 | ```elixir
97 | config :my_app, PhoenixOauth2Provider,
98 | current_resource_owner: :current_user
99 | ```
100 |
101 | ## LICENSE
102 |
103 | (The MIT License)
104 |
105 | Copyright (c) 2017-2019 Dan Schultzer & the Contributors
106 |
107 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
108 |
109 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
110 |
111 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
112 |
--------------------------------------------------------------------------------
/lib/phoenix_oauth2_provider/views/application_view.ex:
--------------------------------------------------------------------------------
1 | defmodule PhoenixOauth2Provider.ApplicationView do
2 | use PhoenixOauth2Provider.View
3 |
4 | template "edit.html",
5 | """
6 | Oops, something went wrong! Please check the errors below.
19 |<%%= @native_redirect_uri %> for local tests
33 |
34 | <%% end %>
35 |
36 | <%%= label f, :scopes %>
37 | <%%= text_input f, :scopes %>
38 | <%%= error_tag f, :scopes %>
39 |
40 | Separate scopes with spaces. Leave blank to use the default scopes.
41 |
42 |
43 | | Name | 57 |Callback URL | 58 |59 | |
|---|---|---|
| <%%= link application.name, to: Routes.oauth_application_path(@conn, :show, application) %> | 65 |<%%= application.redirect_uri %> | 66 |67 | <%%= link "Edit", to: Routes.oauth_application_path(@conn, :edit, application) %> 68 | <%%= link "Delete", to: Routes.oauth_application_path(@conn, :delete, application), method: :delete, data: [confirm: "Are you sure?"] %> 69 | | 70 |
114 | <%%= redirect_uri %>
115 | |
116 | 117 | <%%= link "Authorize", to: Routes.oauth_authorization_path(@conn, :new, client_id: @application.uid, redirect_uri: redirect_uri, response_type: "code", scope: @application.scopes), target: '_blank' %> 118 | | 119 |