├── 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 |

Authorized Applications

7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | <%%= for application <- @applications do %> 18 | 19 | 20 | 21 | 22 | 25 | 26 | <%% end %> 27 | 28 |
ApplicationCreated At
<%%= application.name %><%%= application.inserted_at %> 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 |
29 | """ 30 | end 31 | -------------------------------------------------------------------------------- /lib/phoenix_oauth2_provider/controllers/token_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixOauth2Provider.TokenController do 2 | @moduledoc false 3 | use PhoenixOauth2Provider.Controller, :api 4 | 5 | alias ExOauth2Provider.Token 6 | 7 | @spec create(Conn.t(), map(), keyword()) :: Conn.t() 8 | def create(conn, params, config) do 9 | params 10 | |> Token.grant(config) 11 | |> case do 12 | {:ok, access_token} -> 13 | json(conn, access_token) 14 | 15 | {:error, error, status} -> 16 | conn 17 | |> put_status(status) 18 | |> json(error) 19 | end 20 | end 21 | 22 | @spec revoke(Conn.t(), map(), keyword()) :: Conn.t() 23 | def revoke(conn, params, config) do 24 | params 25 | |> Token.revoke(config) 26 | |> case do 27 | {:ok, response} -> 28 | json(conn, response) 29 | 30 | {:error, error, status} -> 31 | conn 32 | |> put_status(status) 33 | |> json(error) 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /test/phoenix_oauth2_provider/controller_test.exs: -------------------------------------------------------------------------------- 1 | defmodule PhoenixOauth2Provider.ControllerTest do 2 | use PhoenixOauth2Provider.ConnCase 3 | 4 | alias Plug.Conn 5 | alias PhoenixOauth2Provider.Test.Fixtures 6 | 7 | setup %{conn: conn} do 8 | user = Fixtures.user() 9 | conn = Conn.assign(conn, :current_test_user, user) 10 | 11 | {:ok, conn: conn, user: user} 12 | end 13 | 14 | test "handles invalid resource owner", %{conn: conn} do 15 | assert_raise RuntimeError, "Resource owner was not found with :current_test_user assigns", fn -> 16 | conn = Conn.assign(conn, :current_test_user, nil) 17 | 18 | get(conn, Routes.oauth_application_path(conn, :index)) 19 | end 20 | end 21 | 22 | test "handles custom web module", %{conn: conn} do 23 | conn = Conn.put_private(conn, :phoenix_oauth2_provider_config, web_module: DummyWeb) 24 | 25 | conn = get conn, Routes.oauth_application_path(conn, :index) 26 | 27 | assert html_response(conn, 200) 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/phoenix_oauth2_provider/controllers/authorized_application_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixOauth2Provider.AuthorizedApplicationController do 2 | @moduledoc false 3 | use PhoenixOauth2Provider.Controller 4 | alias ExOauth2Provider.Applications 5 | alias Plug.Conn 6 | 7 | @spec index(Conn.t(), map(), map(), keyword()) :: Conn.t() 8 | def index(conn, _params, resource_owner, config) do 9 | applications = Applications.get_authorized_applications_for(resource_owner, config) 10 | 11 | render(conn, "index.html", applications: applications) 12 | end 13 | 14 | @spec delete(Conn.t(), map(), map(), keyword()) :: Conn.t() 15 | def delete(conn, %{"uid" => uid}, resource_owner, config) do 16 | {:ok, _application} = 17 | uid 18 | |> Applications.get_application!(config) 19 | |> Applications.revoke_all_access_tokens_for(resource_owner, config) 20 | 21 | conn 22 | |> put_flash(:info, "Application revoked.") 23 | |> redirect(to: Routes.oauth_authorized_application_path(conn, :index)) 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2017-2019 Dan Schultzer & the Contributors 4 | 5 | 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: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | 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. 10 | -------------------------------------------------------------------------------- /test/support/conn_case.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixOauth2Provider.ConnCase do 2 | @moduledoc """ 3 | This module defines the test case to be used by 4 | tests that require setting up a connection. 5 | 6 | Such tests rely on `Phoenix.ConnTest` and also 7 | import other functionality to make it easier 8 | to build common datastructures and query the data layer. 9 | 10 | Finally, if the test case interacts with the database, 11 | it cannot be async. For this reason, every test runs 12 | inside a transaction which is reset at the beginning 13 | of the test unless the test case is marked as async. 14 | """ 15 | use ExUnit.CaseTemplate 16 | 17 | alias Ecto.Adapters.SQL.Sandbox 18 | alias Dummy.Repo 19 | alias DummyWeb.{Endpoint, Router.Helpers} 20 | alias Phoenix.ConnTest 21 | 22 | using do 23 | quote do 24 | use ConnTest 25 | 26 | alias Helpers, as: Routes 27 | 28 | @endpoint Endpoint 29 | end 30 | end 31 | 32 | setup tags do 33 | unless tags[:async] do 34 | :ok = Sandbox.checkout(Repo) 35 | end 36 | {:ok, conn: ConnTest.build_conn()} 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/mix/tasks/phoenix_oauth2_provider.gen.templates.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.PhoenixOauth2Provider.Gen.Templates do 2 | @shortdoc "Generates PhoenixOauth2Provider templates" 3 | 4 | @moduledoc """ 5 | Generates views and templates. 6 | 7 | mix phoenix_oauth2_provider.gen.templates 8 | 9 | ## Arguments 10 | 11 | * `--context-app` - context app to use for path and module names 12 | """ 13 | use Mix.Task 14 | 15 | alias Mix.ExOauth2Provider 16 | alias Mix.PhoenixOauth2Provider.Template 17 | 18 | @switches [context_app: :string] 19 | @default_opts [] 20 | @mix_task "phoenix_oauth2_provider.gen.templates" 21 | 22 | @impl true 23 | def run(args) do 24 | ExOauth2Provider.no_umbrella!(@mix_task) 25 | 26 | args 27 | |> ExOauth2Provider.parse_options(@switches, @default_opts) 28 | |> parse() 29 | |> create_template_files() 30 | end 31 | 32 | defp parse({config, _parsed, _invalid}), do: config 33 | 34 | defp create_template_files(config) do 35 | config 36 | |> Map.get(:context_app) 37 | |> Kernel.||(ExOauth2Provider.otp_app()) 38 | |> Template.create_view_and_template_files() 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/phoenix_oauth2_provider/config.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixOauth2Provider.Config do 2 | @moduledoc false 3 | 4 | @spec current_resource_owner(keyword()) :: atom() 5 | def current_resource_owner(config), do: get(config, :current_resource_owner, :current_user) 6 | 7 | @spec web_module(keyword()) :: atom() 8 | def web_module(config), do: get(config, :web_module) 9 | 10 | defp get(config, key, value \\ nil) do 11 | otp_app = Keyword.get(config, :otp_app) 12 | 13 | config 14 | |> get_from_config(key) 15 | |> get_from_app_env(otp_app, key) 16 | |> get_from_global_env(key) 17 | |> case do 18 | :not_found -> value 19 | value -> value 20 | end 21 | end 22 | 23 | defp get_from_config(config, key), do: Keyword.get(config, key, :not_found) 24 | 25 | defp get_from_app_env(:not_found, nil, _key), do: :not_found 26 | defp get_from_app_env(:not_found, otp_app, key) do 27 | otp_app 28 | |> Application.get_env(PhoenixOauth2Provider, []) 29 | |> Keyword.get(key, :not_found) 30 | end 31 | defp get_from_app_env(value, _otp_app, _key), do: value 32 | 33 | defp get_from_global_env(:not_found, key) do 34 | :phoenix_oauth2_provider 35 | |> Application.get_env(PhoenixOauth2Provider, []) 36 | |> Keyword.get(key, :not_found) 37 | end 38 | defp get_from_global_env(value, _key), do: value 39 | end 40 | -------------------------------------------------------------------------------- /test/support/fixtures.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixOauth2Provider.Test.Fixtures do 2 | @moduledoc false 3 | 4 | alias Dummy.Repo 5 | alias ExOauth2Provider.{AccessGrants, AccessTokens, Applications, Config} 6 | alias Dummy.Users.User 7 | alias Ecto.Changeset 8 | 9 | def user do 10 | User 11 | |> struct() 12 | |> Changeset.change(%{email: "user@example.com"}) 13 | |> Repo.insert!() 14 | end 15 | 16 | def application(%{user: user} = attrs \\ []) do 17 | attrs = Map.merge(%{name: "Example", redirect_uri: "https://example.com"}, attrs) 18 | {:ok, application} = Applications.create_application(user, attrs, otp_app: :phoenix_oauth2_provider) 19 | 20 | application 21 | end 22 | 23 | def access_token(%{application: application, user: user} = attrs) do 24 | attrs = Map.put_new(attrs, :redirect_uri, application.redirect_uri) 25 | 26 | {:ok, access_token} = AccessTokens.create_token(user, attrs, otp_app: :phoenix_oauth2_provider) 27 | 28 | access_token 29 | end 30 | 31 | def access_grant(%{application: application, user: user} = attrs) do 32 | attrs = 33 | attrs 34 | |> Map.put_new(:redirect_uri, application.redirect_uri) 35 | |> Map.put_new(:expires_in, Config.authorization_code_expires_in(otp_app: :phoenix_oauth2_provider)) 36 | 37 | {:ok, access_token} = AccessGrants.create_grant(user, application, attrs, otp_app: :phoenix_oauth2_provider) 38 | 39 | access_token 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /test/phoenix_oauth2_provider/controllers/authorized_application_controller_test.exs: -------------------------------------------------------------------------------- 1 | defmodule PhoenixOauth2Provider.AuthorizedApplicationControllerTest do 2 | use PhoenixOauth2Provider.ConnCase 3 | 4 | alias PhoenixOauth2Provider.Test.Fixtures 5 | alias Plug.Conn 6 | 7 | setup %{conn: conn} do 8 | user = Fixtures.user() 9 | application = Fixtures.application(%{user: user}) 10 | 11 | conn = Conn.assign(conn, :current_test_user, user) 12 | 13 | {:ok, conn: conn, user: user, application: application} 14 | end 15 | 16 | test "lists all authorized applications on index", %{conn: conn, user: user} do 17 | application1 = Fixtures.application(%{user: Fixtures.user(), name: "Application 1"}) 18 | Fixtures.access_token(%{application: application1, user: user}) 19 | application2 = Fixtures.application(%{user: Fixtures.user(), name: "Application 2"}) 20 | Fixtures.access_token(%{application: application2, user: user}) 21 | application3 = Fixtures.application(%{user: Fixtures.user(), name: "Application 3"}) 22 | 23 | conn = get conn, Routes.oauth_authorized_application_path(conn, :index) 24 | body = html_response(conn, 200) 25 | 26 | assert body =~ "Authorized Applications" 27 | assert body =~ application1.name 28 | assert body =~ application2.name 29 | refute body =~ application3.name 30 | end 31 | 32 | test "deletes chosen authorized application", %{conn: conn, application: application} do 33 | conn = delete conn, Routes.oauth_authorized_application_path(conn, :delete, application) 34 | assert redirected_to(conn) == Routes.oauth_authorized_application_path(conn, :index) 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/phoenix_oauth2_provider/view.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixOauth2Provider.View do 2 | @moduledoc false 3 | 4 | alias Phoenix.HTML.Tag 5 | 6 | @doc false 7 | defmacro __using__(_opts) do 8 | quote do 9 | import unquote(__MODULE__) 10 | import Phoenix.HTML.{Form, Link, Tag} 11 | 12 | alias PhoenixOauth2Provider.Router.Helpers, as: Routes 13 | 14 | Module.register_attribute(__MODULE__, :templates, accumulate: true) 15 | @before_compile unquote(__MODULE__) 16 | end 17 | end 18 | 19 | @doc false 20 | defmacro __before_compile__(_env) do 21 | quote do 22 | @spec templates() :: [binary()] 23 | def templates(), do: @templates 24 | end 25 | end 26 | 27 | defmacro template(template, content) do 28 | content = EEx.eval_string(content) 29 | quoted = EEx.compile_string(content, engine: Phoenix.HTML.Engine, line: 1, trim: true) 30 | 31 | quote do 32 | @templates unquote(template) 33 | 34 | def render(unquote(template), var!(assigns)) do 35 | _ = var!(assigns) 36 | unquote(quoted) 37 | end 38 | 39 | def html(unquote(template)), do: unquote(content) 40 | end 41 | end 42 | 43 | def error_tag(form, field) do 44 | form.errors 45 | |> Keyword.get_values(field) 46 | |> Enum.map(&error_tag/1) 47 | end 48 | def error_tag(error) do 49 | Tag.content_tag(:span, translate_error(error), class: "help-block") 50 | end 51 | 52 | defp translate_error({msg, opts}) do 53 | Enum.reduce(opts, msg, fn {key, value}, msg -> 54 | token = "%{#{key}}" 55 | 56 | case String.contains?(msg, token) do 57 | true -> String.replace(msg, token, to_string(value), global: false) 58 | false -> msg 59 | end 60 | end) 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /test/mix/tasks/phoenix_oauth2_provider.gen.templates_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.PhoenixOauth2Provider.Gen.TemplatesTest do 2 | use PhoenixOauth2Provider.Mix.TestCase 3 | 4 | alias Mix.Tasks.PhoenixOauth2Provider.Gen.Templates 5 | 6 | @tmp_path Path.join(["tmp", inspect(Templates)]) 7 | 8 | @expected_template_files %{ 9 | "application" => ["edit.html.eex", "form.html.eex", "index.html.eex", "new.html.eex", "show.html.eex"], 10 | "authorization" => ["error.html.eex", "new.html.eex", "show.html.eex"], 11 | "authorized_application" => ["index.html.eex"] 12 | } 13 | @expected_views Map.keys(@expected_template_files) 14 | 15 | setup do 16 | File.rm_rf!(@tmp_path) 17 | File.mkdir_p!(@tmp_path) 18 | 19 | :ok 20 | end 21 | 22 | test "generates templates" do 23 | File.cd!(@tmp_path, fn -> 24 | Templates.run([]) 25 | 26 | templates_path = Path.join(["lib", "phoenix_oauth2_provider_web", "templates", "phoenix_oauth2_provider"]) 27 | expected_dirs = Map.keys(@expected_template_files) 28 | 29 | assert ls(templates_path) == expected_dirs 30 | 31 | for {dir, expected_files} <- @expected_template_files do 32 | files = templates_path |> Path.join(dir) |> ls() 33 | assert files == expected_files 34 | end 35 | 36 | views_path = Path.join(["lib", "phoenix_oauth2_provider_web", "views", "phoenix_oauth2_provider"]) 37 | expected_view_files = Enum.map(@expected_views, &"#{&1}_view.ex") 38 | view_content = views_path |> Path.join("application_view.ex") |> File.read!() 39 | 40 | assert ls(views_path) == expected_view_files 41 | assert view_content =~ "defmodule PhoenixOauth2ProviderWeb.PhoenixOauth2Provider.ApplicationView do" 42 | assert view_content =~ "use PhoenixOauth2ProviderWeb, :view" 43 | end) 44 | end 45 | 46 | defp ls(path), do: path |> File.ls!() |> Enum.sort() 47 | end 48 | -------------------------------------------------------------------------------- /lib/phoenix_oauth2_provider/views/authorization_view.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixOauth2Provider.AuthorizationView do 2 | use PhoenixOauth2Provider.View 3 | 4 | template "error.html", 5 | """ 6 |

An error has occurred

7 | 8 |
9 |

<%%= @error[:error_description] %>

10 |
11 | """ 12 | 13 | template "new.html", 14 | """ 15 |

Authorize <%%= @client.name %> to use your account?

16 | 17 |
18 |

This application will be able to:

19 | 24 |
25 | 26 |
27 | <%%= form_tag Routes.oauth_authorization_path(@conn, :create), method: :post do %> 28 | " /> 29 | " /> 30 | " /> 31 | " /> 32 | " /> 33 | <%%= submit "Authorize" %> 34 | <%% end %> 35 | <%%= form_tag Routes.oauth_authorization_path(@conn, :delete), method: :delete do %> 36 | " /> 37 | " /> 38 | " /> 39 | " /> 40 | " /> 41 | <%%= submit "Deny" %> 42 | <%% end %> 43 |
44 | """ 45 | 46 | template "show.html", 47 | """ 48 |

Authorization code

49 | 50 | <%%= @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 | [![Build Status](https://travis-ci.org/danschultzer/phoenix_oauth2_provider.svg?branch=master)](https://travis-ci.org/danschultzer/phoenix_oauth2_provider) [![hex.pm](http://img.shields.io/hexpm/v/phoenix_oauth2_provider.svg?style=flat)](https://hex.pm/packages/phoenix_oauth2_provider) [![hex.pm downloads](https://img.shields.io/hexpm/dt/phoenix_oauth2_provider.svg?style=flat)](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 |

Edit Application

7 | 8 | <%%= render "form.html", Map.put(assigns, :action, Routes.oauth_application_path(@conn, :update, @changeset.data)) %> 9 | 10 | <%%= link "Back", to: Routes.oauth_application_path(@conn, :index) %> 11 | """ 12 | 13 | template "form.html", 14 | """ 15 | <%%= form_for @changeset, @action, fn f -> %> 16 | <%%= if @changeset.action do %> 17 |
18 |

Oops, something went wrong! Please check the errors below.

19 |
20 | <%% end %> 21 | 22 | <%%= label f, :name %> 23 | <%%= text_input f, :name %> 24 | <%%= error_tag f, :name %> 25 | 26 | <%%= label f, :redirect_uri %> 27 | <%%= textarea f, :redirect_uri %> 28 | <%%= error_tag f, :redirect_uri %> 29 | Use one line per URI 30 | <%%= if is_nil(@native_redirect_uri) do %> 31 | 32 | Use <%%= @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 |
44 | <%%= submit "Save" %> 45 |
46 | <%% end %> 47 | """ 48 | 49 | template "index.html", 50 | """ 51 |

Your applications

52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | <%%= for application <- @applications do %> 63 | 64 | 65 | 66 | 70 | 71 | <%% end %> 72 | 73 |
NameCallback URL
<%%= link application.name, to: Routes.oauth_application_path(@conn, :show, application) %><%%= application.redirect_uri %> 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 |
74 | 75 | <%%= link "New Application", to: Routes.oauth_application_path(@conn, :new) %> 76 | """ 77 | 78 | template "new.html", 79 | """ 80 |

New Application

81 | 82 | <%%= render "form.html", Map.put(assigns, :action, Routes.oauth_application_path(@conn, :create)) %> 83 | 84 | <%%= link "Back", to: Routes.oauth_application_path(@conn, :index) %> 85 | """ 86 | 87 | template "show.html", """ 88 |

Show Application

89 | 90 | 125 | 126 | <%%= link "Edit", to: Routes.oauth_application_path(@conn, :edit, @application) %> 127 | <%%= link "Back", to: Routes.oauth_application_path(@conn, :index) %> 128 | """ 129 | end 130 | -------------------------------------------------------------------------------- /lib/phoenix_oauth2_provider/router.ex: -------------------------------------------------------------------------------- 1 | defmodule PhoenixOauth2Provider.Router do 2 | @moduledoc """ 3 | Handles routes for PhoenixOauth2Provider. 4 | 5 | ## Usage 6 | 7 | Configure `lib/my_app_web/router.ex` the following way: 8 | 9 | defmodule MyAppWeb.Router do 10 | use MyAppWeb, :router 11 | use PhoenixOauth2Provider.Router 12 | 13 | pipeline :browser do 14 | plug :accepts, ["html"] 15 | plug :fetch_session 16 | plug :fetch_flash 17 | plug :protect_from_forgery 18 | plug :put_secure_browser_headers 19 | end 20 | 21 | pipeline :api do 22 | plug :accepts, ["json"] 23 | end 24 | 25 | pipeline :protected do 26 | # Require user authentication 27 | end 28 | 29 | scope "/" do 30 | pipe_through [:browser, :protected] 31 | 32 | oauth_routes() 33 | end 34 | 35 | scope "/" do 36 | pipe_through :api 37 | 38 | oauth_api_routes() 39 | end 40 | 41 | # ... 42 | end 43 | """ 44 | 45 | defmacro __using__(config \\ []) do 46 | quote do 47 | @phoenix_oauth2_provider_config unquote(config) 48 | import unquote(__MODULE__) 49 | end 50 | end 51 | 52 | @doc """ 53 | OAuth 2.0 browser routes macro. 54 | 55 | Use this macro to define the protected browser oauth routes. 56 | 57 | ## Example 58 | 59 | scope "/" do 60 | pipe_through [:browser, :protected] 61 | 62 | oauth_routes() 63 | end 64 | """ 65 | defmacro oauth_routes(options \\ []) do 66 | quote location: :keep do 67 | oauth_scope unquote(options), @phoenix_oauth2_provider_config do 68 | scope "/authorize" do 69 | get "/", AuthorizationController, :new 70 | post "/", AuthorizationController, :create 71 | get "/:code", AuthorizationController, :show 72 | delete "/", AuthorizationController, :delete 73 | end 74 | resources "/applications", ApplicationController, param: "uid" 75 | resources "/authorized_applications", AuthorizedApplicationController, only: [:index, :delete], param: "uid" 76 | end 77 | end 78 | end 79 | 80 | @doc """ 81 | OAuth 2.0 API routes macro. 82 | 83 | Use this macro to define the public API oauth routes. These routes 84 | should not have CSRF protection. 85 | 86 | ## Example 87 | 88 | scope "/" do 89 | pipe_through :api 90 | 91 | oauth_api_routes() 92 | end 93 | """ 94 | defmacro oauth_api_routes(options \\ []) do 95 | quote location: :keep do 96 | oauth_scope unquote(options), @phoenix_oauth2_provider_config do 97 | post "/token", TokenController, :create 98 | post "/revoke", TokenController, :revoke 99 | end 100 | end 101 | end 102 | 103 | @doc false 104 | defmacro oauth_scope(options \\ [], config \\ [], do: context) do 105 | quote do 106 | path = Keyword.get(unquote(options), :path, "oauth") 107 | 108 | scope "/#{path}", PhoenixOauth2Provider, as: "oauth", private: %{phoenix_oauth2_provider_config: unquote(config)} do 109 | unquote(context) 110 | end 111 | end 112 | end 113 | 114 | defmodule Helpers do 115 | @moduledoc false 116 | 117 | alias Plug.Conn 118 | alias PhoenixOauth2Provider.Controller 119 | 120 | @spec oauth_application_path(Conn.t(), atom()) :: binary() 121 | def oauth_application_path(conn, action), do: Controller.routes(conn).oauth_application_path(conn, action) 122 | 123 | @spec oauth_application_path(Conn.t(), atom(), map()) :: binary() 124 | def oauth_application_path(conn, action, application), do: Controller.routes(conn).oauth_application_path(conn, action, application) 125 | 126 | @spec oauth_authorization_path(Conn.t(), atom()) :: binary() 127 | def oauth_authorization_path(conn, action), do: Controller.routes(conn).oauth_authorization_path(conn, action) 128 | 129 | @spec oauth_authorization_path(Conn.t(), atom(), binary()) :: binary() 130 | def oauth_authorization_path(conn, action, code), do: Controller.routes(conn).oauth_authorization_path(conn, action, code) 131 | 132 | @spec oauth_application_path(Conn.t(), atom()) :: binary() 133 | def oauth_authorized_application_path(conn, action), do: Controller.routes(conn).oauth_authorized_application_path(conn, action) 134 | 135 | @spec oauth_application_path(Conn.t(), atom(), map()) :: binary() 136 | def oauth_authorized_application_path(conn, action, application), do: Controller.routes(conn).oauth_authorized_application_path(conn, action, application) 137 | end 138 | end 139 | -------------------------------------------------------------------------------- /test/phoenix_oauth2_provider/controllers/authorization_controller_test.exs: -------------------------------------------------------------------------------- 1 | defmodule PhoenixOauth2Provider.AuthorizationControllerTest do 2 | use PhoenixOauth2Provider.ConnCase 3 | 4 | alias Dummy.{OauthAccessGrants.OauthAccessGrant, Repo} 5 | alias ExOauth2Provider.Scopes 6 | alias PhoenixOauth2Provider.Test.Fixtures 7 | alias Plug.Conn 8 | 9 | setup %{conn: conn} do 10 | user = Fixtures.user() 11 | conn = Conn.assign(conn, :current_test_user, user) 12 | {:ok, conn: conn, user: user} 13 | end 14 | 15 | test "new/2 renders authorization form", %{conn: conn, user: user} do 16 | application = Fixtures.application(%{user: user}) 17 | 18 | conn = get conn, Routes.oauth_authorization_path(conn, :new, valid_request(application)) 19 | body = html_response(conn, 200) 20 | 21 | assert body =~ "Authorize #{application.name} to use your account?" 22 | assert body =~ application.name 23 | application.scopes 24 | |> Scopes.to_list() 25 | |> Enum.each(fn(scope) -> 26 | assert body =~ "
  • #{scope}
  • " 27 | end) 28 | end 29 | 30 | test "new/2 renders error with invalid client", %{conn: conn} do 31 | conn = get conn, Routes.oauth_authorization_path(conn, :new, %{client_id: "", response_type: "code"}) 32 | assert html_response(conn, 422) =~ "Client authentication failed due to unknown client, no client authentication included, or unsupported authentication method." 33 | end 34 | 35 | test "new/2 redirects with error", %{conn: conn, user: user} do 36 | application = Fixtures.application(%{user: user}) 37 | conn = get conn, Routes.oauth_authorization_path(conn, :new, %{client_id: application.uid, response_type: "other"}) 38 | assert redirected_to(conn) == "https://example.com?error=unsupported_response_type&error_description=The+authorization+server+does+not+support+this+response+type." 39 | end 40 | 41 | test "new/2 with matching access token redirects when already shown", %{conn: conn, user: user} do 42 | application = Fixtures.application(%{user: user}) 43 | Fixtures.access_token(%{user: user, application: application}) 44 | 45 | conn = get conn, Routes.oauth_authorization_path(conn, :new, valid_request(application)) 46 | assert redirected_to(conn) == "https://example.com?code=#{last_grant_token()}" 47 | end 48 | 49 | test "create/2 redirects", %{conn: conn, user: user} do 50 | application = Fixtures.application(%{user: user}) 51 | conn = post conn, Routes.oauth_authorization_path(conn, :create, valid_request(application)) 52 | assert redirected_to(conn) == "https://example.com?code=#{last_grant_token()}" 53 | 54 | assert last_grant().resource_owner_id == user.id 55 | end 56 | 57 | test "delete/2 redirects", %{conn: conn, user: user} do 58 | application = Fixtures.application(%{user: user}) 59 | conn = delete conn, Routes.oauth_authorization_path(conn, :delete, valid_request(application)) 60 | assert redirected_to(conn) == "https://example.com?error=access_denied&error_description=The+resource+owner+or+authorization+server+denied+the+request." 61 | end 62 | 63 | describe "application with native redirect uri" do 64 | setup %{conn: conn, user: user} do 65 | application = Fixtures.application(%{user: user, redirect_uri: "urn:ietf:wg:oauth:2.0:oob"}) 66 | 67 | {:ok, conn: conn, user: user, application: application} 68 | end 69 | 70 | test "new/2 redirects to native", %{conn: authed_conn, user: user, application: application} do 71 | Fixtures.access_token(%{user: user, application: application}) 72 | 73 | conn = get authed_conn, Routes.oauth_authorization_path(authed_conn, :new, valid_request(application)) 74 | assert redirected_to(conn) == Routes.oauth_authorization_path(conn, :show, last_grant_token()) 75 | 76 | conn = get authed_conn, Routes.oauth_authorization_path(conn, :show, last_grant_token()) 77 | assert html_response(conn, 200) =~ last_grant_token() 78 | end 79 | 80 | test "create/2 shows json", %{conn: conn, application: application} do 81 | conn = post conn, Routes.oauth_authorization_path(conn, :create, valid_request(application)) 82 | body = json_response(conn, 200) 83 | assert last_grant_token() == body["code"] 84 | end 85 | 86 | test "delete/2 shows json", %{conn: conn, application: application} do 87 | conn = delete conn, Routes.oauth_authorization_path(conn, :delete, valid_request(application)) 88 | body = json_response(conn, 401) 89 | assert "The resource owner or authorization server denied the request." == body["error_description"] 90 | end 91 | end 92 | 93 | defp valid_request(%{uid: uid}), do: %{client_id: uid, response_type: "code"} 94 | 95 | defp last_grant do 96 | OauthAccessGrant 97 | |> Repo.all() 98 | |> List.last() 99 | end 100 | 101 | defp last_grant_token, do: last_grant().token 102 | end 103 | -------------------------------------------------------------------------------- /test/phoenix_oauth2_provider/controllers/token_controller_test.exs: -------------------------------------------------------------------------------- 1 | defmodule PhoenixOauth2Provider.TokenControllerTest do 2 | use PhoenixOauth2Provider.ConnCase 3 | 4 | alias Dummy.Repo 5 | alias PhoenixOauth2Provider.Test.Fixtures 6 | alias ExOauth2Provider.AccessTokens 7 | alias Dummy.OauthAccessTokens.OauthAccessToken 8 | 9 | setup %{conn: conn} do 10 | application = Fixtures.application(%{user: Fixtures.user()}) 11 | 12 | {:ok, conn: conn, application: application} 13 | end 14 | 15 | describe "with authorization_code strategy" do 16 | setup %{conn: conn, application: application} do 17 | user = Fixtures.user() 18 | access_grant = Fixtures.access_grant(%{user: user, application: application}) 19 | request = %{client_id: application.uid, 20 | client_secret: application.secret, 21 | grant_type: "authorization_code", 22 | redirect_uri: application.redirect_uri, 23 | code: access_grant.token} 24 | 25 | {:ok, conn: conn, request: request} 26 | end 27 | 28 | test "create/2", %{conn: conn, request: request} do 29 | conn = post conn, Routes.oauth_token_path(conn, :create, request) 30 | body = json_response(conn, 200) 31 | assert last_access_token() == body["access_token"] 32 | end 33 | 34 | test "create/2 with error", %{conn: conn, request: request} do 35 | conn = post conn, Routes.oauth_token_path(conn, :create), Map.merge(request, %{redirect_uri: "invalid"}) 36 | body = json_response(conn, 422) 37 | assert "The provided authorization grant is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client." == body["error_description"] 38 | end 39 | end 40 | 41 | describe "as client_credentials strategy" do 42 | setup %{conn: conn, application: application} do 43 | request = %{client_id: application.uid, 44 | client_secret: application.secret, 45 | grant_type: "client_credentials"} 46 | 47 | {:ok, conn: conn, request: request} 48 | end 49 | 50 | test "create/2", %{conn: conn, request: request} do 51 | conn = post conn, Routes.oauth_token_path(conn, :create, request) 52 | 53 | body = json_response(conn, 200) 54 | assert last_access_token() == body["access_token"] 55 | assert is_nil(body["refresh_token"]) 56 | end 57 | 58 | test "create/2 with error", %{conn: conn, request: request} do 59 | conn = post conn, Routes.oauth_token_path(conn, :create, Map.merge(request, %{client_id: "invalid"})) 60 | body = json_response(conn, 422) 61 | assert "Client authentication failed due to unknown client, no client authentication included, or unsupported authentication method." == body["error_description"] 62 | end 63 | end 64 | 65 | describe "as refresh_token strategy" do 66 | setup %{conn: conn, application: application} do 67 | user = Fixtures.user() 68 | access_token = Fixtures.access_token(%{application: application, user: user, use_refresh_token: true}) 69 | request = %{client_id: application.uid, 70 | client_secret: application.secret, 71 | grant_type: "refresh_token", 72 | refresh_token: access_token.refresh_token} 73 | 74 | {:ok, conn: conn, request: request} 75 | end 76 | 77 | test "create/2", %{conn: conn, request: request} do 78 | conn = post conn, Routes.oauth_token_path(conn, :create, request) 79 | 80 | body = json_response(conn, 200) 81 | assert last_access_token() == body["access_token"] 82 | refute is_nil(body["refresh_token"]) 83 | end 84 | 85 | test "create/2 with error", %{conn: conn, request: request} do 86 | conn = post conn, Routes.oauth_token_path(conn, :create, Map.merge(request, %{client_id: "invalid"})) 87 | body = json_response(conn, 422) 88 | assert "Client authentication failed due to unknown client, no client authentication included, or unsupported authentication method." == body["error_description"] 89 | end 90 | end 91 | 92 | describe "with revocation strategy" do 93 | setup %{conn: conn, application: application} do 94 | user = Fixtures.user() 95 | access_token = Fixtures.access_token(%{application: application, user: user}) 96 | request = %{client_id: application.uid, 97 | client_secret: application.secret, 98 | token: access_token.token} 99 | 100 | {:ok, conn: conn, request: request} 101 | end 102 | 103 | test "revoke/2", %{conn: conn, request: request} do 104 | conn = post conn, Routes.oauth_token_path(conn, :revoke, request) 105 | body = json_response(conn, 200) 106 | assert body == %{} 107 | assert AccessTokens.is_revoked?(last_access_token()) 108 | end 109 | 110 | test "revoke/2 with invalid token", %{conn: conn, request: request} do 111 | conn = post conn, Routes.oauth_token_path(conn, :revoke, Map.merge(request, %{token: "invalid"})) 112 | body = json_response(conn, 200) 113 | assert body == %{} 114 | end 115 | end 116 | 117 | defp last_access_token do 118 | OauthAccessToken 119 | |> Repo.all() 120 | |> List.last() 121 | |> Map.get(:token) 122 | end 123 | end 124 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm"}, 3 | "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm"}, 4 | "cowboy": {:hex, :cowboy, "2.6.3", "99aa50e94e685557cad82e704457336a453d4abcb77839ad22dbe71f311fcc06", [:rebar3], [{:cowlib, "~> 2.7.3", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm"}, 5 | "cowlib": {:hex, :cowlib, "2.7.3", "a7ffcd0917e6d50b4d5fb28e9e2085a0ceb3c97dea310505f7460ff5ed764ce9", [:rebar3], [], "hexpm"}, 6 | "credo": {:hex, :credo, "1.1.0", "e0c07b2fd7e2109495f582430a1bc96b2c71b7d94c59dfad120529f65f19872f", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"}, 7 | "db_connection": {:hex, :db_connection, "2.0.6", "bde2f85d047969c5b5800cb8f4b3ed6316c8cb11487afedac4aa5f93fd39abfa", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm"}, 8 | "decimal": {:hex, :decimal, "1.7.0", "30d6b52c88541f9a66637359ddf85016df9eb266170d53105f02e4a67e00c5aa", [:mix], [], "hexpm"}, 9 | "earmark": {:hex, :earmark, "1.3.2", "b840562ea3d67795ffbb5bd88940b1bed0ed9fa32834915125ea7d02e35888a5", [:mix], [], "hexpm"}, 10 | "ecto": {:hex, :ecto, "3.0.9", "f01922a0b91a41d764d4e3a914d7f058d99a03460d3082c61dd2dcadd724c934", [:mix], [{:decimal, "~> 1.6", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm"}, 11 | "ecto_sql": {:hex, :ecto_sql, "3.0.0", "8d1883376bee02a0e76b5ef797e39d04333c34b9935d0b4785dbf3cbdb571e2a", [:mix], [{:db_connection, "~> 2.0", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.0.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:mariaex, "~> 0.9.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.14.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.2.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm"}, 12 | "ex_doc": {:hex, :ex_doc, "0.20.2", "1bd0dfb0304bade58beb77f20f21ee3558cc3c753743ae0ddbb0fd7ba2912331", [:mix], [{:earmark, "~> 1.3", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.10", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"}, 13 | "ex_oauth2_provider": {:hex, :ex_oauth2_provider, "0.5.1", "4c806c7f996e9f54a76b2078b70a0dfc749ffd684fb397d158aca43d05be1ec5", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:plug, ">= 1.5.0 and < 2.0.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, 14 | "jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"}, 15 | "makeup": {:hex, :makeup, "0.8.0", "9cf32aea71c7fe0a4b2e9246c2c4978f9070257e5c9ce6d4a28ec450a839b55f", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"}, 16 | "makeup_elixir": {:hex, :makeup_elixir, "0.13.0", "be7a477997dcac2e48a9d695ec730b2d22418292675c75aa2d34ba0909dcdeda", [:mix], [{:makeup, "~> 0.8", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm"}, 17 | "mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm"}, 18 | "nimble_parsec": {:hex, :nimble_parsec, "0.5.0", "90e2eca3d0266e5c53f8fbe0079694740b9c91b6747f2b7e3c5d21966bba8300", [:mix], [], "hexpm"}, 19 | "phoenix": {:hex, :phoenix, "1.4.4", "5d9a0e2c3443bb0b36819657711ade39fe9777e11892729268009bb90332e74e", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}], "hexpm"}, 20 | "phoenix_ecto": {:hex, :phoenix_ecto, "4.0.0", "c43117a136e7399ea04ecaac73f8f23ee0ffe3e07acfcb8062fe5f4c9f0f6531", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.9", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, 21 | "phoenix_html": {:hex, :phoenix_html, "2.13.2", "f5d27c9b10ce881a60177d2b5227314fc60881e6b66b41dfe3349db6ed06cf57", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, 22 | "phoenix_pubsub": {:hex, :phoenix_pubsub, "1.1.2", "496c303bdf1b2e98a9d26e89af5bba3ab487ba3a3735f74bf1f4064d2a845a3e", [:mix], [], "hexpm"}, 23 | "plug": {:hex, :plug, "1.7.2", "d7b7db7fbd755e8283b6c0a50be71ec0a3d67d9213d74422d9372effc8e87fd1", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}], "hexpm"}, 24 | "plug_cowboy": {:hex, :plug_cowboy, "2.0.2", "6055f16868cc4882b24b6e1d63d2bada94fb4978413377a3b32ac16c18dffba2", [:mix], [{:cowboy, "~> 2.5", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, 25 | "plug_crypto": {:hex, :plug_crypto, "1.0.0", "18e49317d3fa343f24620ed22795ec29d4a5e602d52d1513ccea0b07d8ea7d4d", [:mix], [], "hexpm"}, 26 | "postgrex": {:hex, :postgrex, "0.14.3", "5754dee2fdf6e9e508cbf49ab138df964278700b764177e8f3871e658b345a1e", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.0", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm"}, 27 | "ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm"}, 28 | "telemetry": {:hex, :telemetry, "0.2.0", "5b40caa3efe4deb30fb12d7cd8ed4f556f6d6bd15c374c2366772161311ce377", [:mix], [], "hexpm"}, 29 | } 30 | --------------------------------------------------------------------------------