diff --git a/lib/ex_oauth2_provider/access_grants/access_grant.ex b/lib/ex_oauth2_provider/access_grants/access_grant.ex index 6f26d59c..e0533491 100644 --- a/lib/ex_oauth2_provider/access_grants/access_grant.ex +++ b/lib/ex_oauth2_provider/access_grants/access_grant.ex @@ -46,11 +46,33 @@ defmodule ExOauth2Provider.AccessGrants.AccessGrant do ] end + @doc false + def access_grant_allowed_fields do + [:redirect_uri, :expires_in, :scopes] + end + + @doc false + def access_grant_required_fields do + [:redirect_uri, :expires_in, :token, :resource_owner, :application] + end + + @doc false + def access_grant_request_fields do + ["redirect_uri", "scope"] + end + defmacro __using__(config) do quote do + use ExOauth2Provider.Changeset use ExOauth2Provider.Schema, unquote(config) - import unquote(__MODULE__), only: [access_grant_fields: 0] + import unquote(__MODULE__), + only: [ + access_grant_fields: 0, + access_grant_allowed_fields: 0, + access_grant_required_fields: 0, + access_grant_request_fields: 0 + ] end end @@ -61,18 +83,18 @@ defmodule ExOauth2Provider.AccessGrants.AccessGrant do end alias Ecto.Changeset - alias ExOauth2Provider.{Mixin.Scopes, Utils} + alias ExOauth2Provider.{Config, Mixin.Scopes, Utils} @spec changeset(Ecto.Schema.t(), map(), keyword()) :: Changeset.t() def changeset(grant, params, config) do grant - |> Changeset.cast(params, [:redirect_uri, :expires_in, :scopes]) + |> Changeset.cast(params, Config.access_grant(config).allowed_fields()) |> Changeset.assoc_constraint(:application) |> Changeset.assoc_constraint(:resource_owner) |> put_token() |> Scopes.put_scopes(grant.application.scopes, config) |> Scopes.validate_scopes(grant.application.scopes, config) - |> Changeset.validate_required([:redirect_uri, :expires_in, :token, :resource_owner, :application]) + |> Changeset.validate_required(Config.access_grant(config).required_fields()) |> Changeset.unique_constraint(:token) end diff --git a/lib/ex_oauth2_provider/access_tokens/access_token.ex b/lib/ex_oauth2_provider/access_tokens/access_token.ex index 49242a3e..164fc316 100644 --- a/lib/ex_oauth2_provider/access_tokens/access_token.ex +++ b/lib/ex_oauth2_provider/access_tokens/access_token.ex @@ -49,11 +49,33 @@ defmodule ExOauth2Provider.AccessTokens.AccessToken do ] end + @doc false + def access_token_allowed_fields do + [:expires_in, :scopes] + end + + @doc false + def access_token_required_fields do + [:expires_in] + end + + @doc false + def access_token_request_fields do + [] + end + defmacro __using__(config) do quote do + use ExOauth2Provider.Changeset use ExOauth2Provider.Schema, unquote(config) - import unquote(__MODULE__), only: [access_token_fields: 0] + import unquote(__MODULE__), + only: [ + access_token_fields: 0, + access_token_allowed_fields: 0, + access_token_required_fields: 0, + access_token_request_fields: 0 + ] end end @@ -71,13 +93,14 @@ defmodule ExOauth2Provider.AccessTokens.AccessToken do server_scopes = server_scopes(token) token - |> Changeset.cast(params, [:expires_in, :scopes]) + |> Changeset.cast(params, Config.access_token(config).allowed_fields()) |> validate_application_or_resource_owner() |> put_previous_refresh_token(params[:previous_refresh_token]) |> put_refresh_token(params[:use_refresh_token]) |> Scopes.put_scopes(server_scopes, config) |> Scopes.validate_scopes(server_scopes, config) |> put_token(config) + |> Changeset.validate_required(Config.access_token(config).required_fields()) end defp server_scopes(%{application: %{scopes: scopes}}), do: scopes diff --git a/lib/ex_oauth2_provider/changeset.ex b/lib/ex_oauth2_provider/changeset.ex new file mode 100644 index 00000000..c5235116 --- /dev/null +++ b/lib/ex_oauth2_provider/changeset.ex @@ -0,0 +1,14 @@ +defmodule ExOauth2Provider.Changeset do + @moduledoc """ + This module defines behaviour for oauth changesets + """ + @callback allowed_fields() :: nonempty_list(atom()) + @callback required_fields() :: nonempty_list(atom()) + @callback request_fields() :: nonempty_list(binary()) + + defmacro __using__(_) do + quote do + @behaviour ExOauth2Provider.Changeset + end + end +end diff --git a/lib/ex_oauth2_provider/oauth2/authorization/strategy/code.ex b/lib/ex_oauth2_provider/oauth2/authorization/strategy/code.ex index 40b3cb02..5eb94c13 100644 --- a/lib/ex_oauth2_provider/oauth2/authorization/strategy/code.ex +++ b/lib/ex_oauth2_provider/oauth2/authorization/strategy/code.ex @@ -104,7 +104,13 @@ defmodule ExOauth2Provider.Authorization.Code do end defp reissue_grant({:error, params}, _config), do: {:error, params} - defp reissue_grant({:ok, %{access_token: _access_token} = params}, config), do: issue_grant({:ok, params}, config) + defp reissue_grant({:ok, %{access_token: access_token} = params}, config) do + params = + access_token + |> Map.take(Config.access_token(config).allowed_fields()) + |> Map.merge(params) + issue_grant({:ok, params}, config) + end defp reissue_grant({:ok, params}, _config), do: {:ok, params} @doc """ @@ -142,18 +148,21 @@ defmodule ExOauth2Provider.Authorization.Code do defp issue_grant({:ok, %{resource_owner: resource_owner, client: application, request: request} = params}, config) do grant_params = request - |> Map.take(["redirect_uri", "scope"]) + |> Map.take(Config.access_grant(config).request_fields()) + |> Map.merge(Map.take(params, Config.access_grant(config).allowed_fields())) |> Map.new(fn {k, v} -> case k do "scope" -> {:scopes, v} - _ -> {String.to_atom(k), v} + "" <> _ -> {String.to_atom(k), v} + _ -> {k, v} end end) |> Map.put(:expires_in, Config.authorization_code_expires_in(config)) case AccessGrants.create_grant(resource_owner, application, grant_params, config) do {:ok, grant} -> {:ok, Map.put(params, :grant, grant)} - {:error, error} -> Error.add_error({:ok, params}, error) + {:error, {:error, _, _} = error} -> Error.add_error({:ok, params}, error) + {:error, _} -> Error.add_error({:ok, params}, Error.server_error()) end end diff --git a/lib/ex_oauth2_provider/oauth2/token/strategy/authorization_code.ex b/lib/ex_oauth2_provider/oauth2/token/strategy/authorization_code.ex index d1c1abe7..375923db 100644 --- a/lib/ex_oauth2_provider/oauth2/token/strategy/authorization_code.ex +++ b/lib/ex_oauth2_provider/oauth2/token/strategy/authorization_code.ex @@ -37,14 +37,23 @@ defmodule ExOauth2Provider.Token.AuthorizationCode do end defp issue_access_token_by_grant({:error, params}, _config), do: {:error, params} - defp issue_access_token_by_grant({:ok, %{access_grant: access_grant, request: _} = params}, config) do - token_params = %{use_refresh_token: Config.use_refresh_token?(config)} - result = Config.repo(config).transaction(fn -> - access_grant - |> revoke_grant(config) - |> maybe_create_access_token(token_params, config) - end) + defp issue_access_token_by_grant( + {:ok, %{access_grant: access_grant, request: request} = params}, + config + ) do + token_params = + request + |> Map.take(Config.access_token(config).request_fields()) + |> Map.new(fn {k, v} -> {String.to_atom(k), v} end) + |> Map.put(:use_refresh_token, Config.use_refresh_token?(config)) + + result = + Config.repo(config).transaction(fn -> + access_grant + |> revoke_grant(config) + |> maybe_create_access_token(token_params, config) + end) case result do {:ok, {:error, error}} -> Error.add_error({:ok, params}, error) @@ -57,8 +66,19 @@ defmodule ExOauth2Provider.Token.AuthorizationCode do do: AccessGrants.revoke(access_grant, config) defp maybe_create_access_token({:error, _} = error, _token_params, _config), do: error - defp maybe_create_access_token({:ok, %{resource_owner: resource_owner, application: application, scopes: scopes}}, token_params, config) do - token_params = Map.merge(token_params, %{scopes: scopes, application: application}) + + defp maybe_create_access_token( + {:ok, + %{resource_owner: resource_owner, application: application, scopes: scopes} = grant}, + token_params, + config + ) do + token_params = + grant + |> Map.drop([:expires_in, :scopes]) + |> Map.take(Config.access_token(config).allowed_fields()) + |> Map.merge(token_params) + |> Map.merge(%{scopes: scopes, application: application}) resource_owner |> AccessTokens.get_token_for(application, scopes, config) diff --git a/lib/ex_oauth2_provider/oauth2/token/strategy/refresh_token.ex b/lib/ex_oauth2_provider/oauth2/token/strategy/refresh_token.ex index dc4df7cf..2050d228 100644 --- a/lib/ex_oauth2_provider/oauth2/token/strategy/refresh_token.ex +++ b/lib/ex_oauth2_provider/oauth2/token/strategy/refresh_token.ex @@ -49,17 +49,29 @@ defmodule ExOauth2Provider.Token.RefreshToken do defp load_access_token_by_refresh_token(params, _config), do: Error.add_error(params, Error.invalid_request()) defp issue_access_token_by_refresh_token({:error, params}, _config), do: {:error, params} - defp issue_access_token_by_refresh_token({:ok, %{refresh_token: refresh_token, request: _} = params}, config) do - result = Config.repo(config).transaction(fn -> - token_params = token_params(refresh_token, config) - refresh_token - |> revoke_access_token(config) - |> case do - {:ok, %{resource_owner: resource_owner}} -> AccessTokens.create_token(resource_owner, token_params, config) - {:error, error} -> {:error, error} - end - end) + defp issue_access_token_by_refresh_token( + {:ok, %{refresh_token: refresh_token, request: request} = params}, + config + ) do + result = + Config.repo(config).transaction(fn -> + token_params = + request + |> Map.take(Config.access_token(config).request_fields()) + |> Map.new(fn {k, v} -> {String.to_atom(k), v} end) + |> Map.merge(token_params(refresh_token, config)) + + refresh_token + |> revoke_access_token(config) + |> case do + {:ok, %{resource_owner: resource_owner}} -> + AccessTokens.create_token(resource_owner, token_params, config) + + {:error, error} -> + {:error, error} + end + end) case result do {:ok, {:error, error}} -> Error.add_error({:ok, params}, error) @@ -69,7 +81,11 @@ defmodule ExOauth2Provider.Token.RefreshToken do end defp token_params(%{scopes: scopes, application: application} = refresh_token, config) do - params = %{scopes: scopes, application: application, use_refresh_token: true} + params = + refresh_token + |> Map.drop([:expires_in, :scopes]) + |> Map.take(Config.access_token(config).allowed_fields()) + |> Map.merge(%{scopes: scopes, application: application, use_refresh_token: true}) case Config.refresh_token_revoked_on_use?(config) do true -> Map.put(params, :previous_refresh_token, refresh_token) diff --git a/lib/mix/ex_oauth2_provider/schema.ex b/lib/mix/ex_oauth2_provider/schema.ex index ceced4d6..7d3f1167 100644 --- a/lib/mix/ex_oauth2_provider/schema.ex +++ b/lib/mix/ex_oauth2_provider/schema.ex @@ -11,21 +11,40 @@ defmodule Mix.ExOauth2Provider.Schema do <%= if schema.binary_id do %> @primary_key {:id, :binary_id, autogenerate: true} @foreign_key_type :binary_id<% end %> - schema <%= inspect schema.table %> do - <%= schema.macro_fields %>() + schema <%= inspect schema.table_name %> do + <%= schema.table %>_fields() timestamps() + end<%= if schema.changeset do %> + + @impl ExOauth2Provider.Changeset + def allowed_fields do + <%= schema.table %>_allowed_fields() + end + + @impl ExOauth2Provider.Changeset + def required_fields do + <%= schema.table %>_required_fields() end + + @impl ExOauth2Provider.Changeset + def request_fields do + <%= schema.table %>_request_fields() + end<% end %> end """ alias ExOauth2Provider.{AccessGrants.AccessGrant, AccessTokens.AccessToken, Applications.Application} - @schemas [{"application", Application}, {"access_grant", AccessGrant}, {"access_token", AccessToken}] + @schemas [ + {"application", Application, false}, + {"access_grant", AccessGrant, true}, + {"access_token", AccessToken, true} + ] @spec create_schema_files(atom(), binary(), keyword()) :: any() def create_schema_files(context_app, namespace, opts) do - for {table, schema} <- @schemas do + for {table, schema, changeset} <- @schemas do app_base = Config.app_base(context_app) table_name = "#{namespace}_#{table}s" context = Macro.camelize(table_name) @@ -34,8 +53,7 @@ defmodule Mix.ExOauth2Provider.Schema do module = Module.concat([app_base, context, module]) binary_id = Keyword.get(opts, :binary_id, false) macro = schema - macro_fields = "#{table}_fields" - content = EEx.eval_string(@template, schema: %{module: module, table: table_name, binary_id: binary_id, macro: macro, macro_fields: macro_fields}, otp_app: context_app) + content = EEx.eval_string(@template, schema: %{module: module, table: table, table_name: table_name, binary_id: binary_id, macro: macro, changeset: changeset}, otp_app: context_app) dir = "lib/#{context_app}/#{Macro.underscore(context)}/" File.mkdir_p!(dir) diff --git a/test/support/lib/dummy/oauth_access_grants/oauth_access_grant.ex b/test/support/lib/dummy/oauth_access_grants/oauth_access_grant.ex index 8b330c15..35d0e05b 100644 --- a/test/support/lib/dummy/oauth_access_grants/oauth_access_grant.ex +++ b/test/support/lib/dummy/oauth_access_grants/oauth_access_grant.ex @@ -13,4 +13,19 @@ defmodule Dummy.OauthAccessGrants.OauthAccessGrant do access_grant_fields() timestamps() end + + @impl ExOauth2Provider.Changeset + def allowed_fields do + access_grant_allowed_fields() + end + + @impl ExOauth2Provider.Changeset + def required_fields do + access_grant_required_fields() + end + + @impl ExOauth2Provider.Changeset + def request_fields do + access_grant_request_fields() + end end diff --git a/test/support/lib/dummy/oauth_access_tokens/oauth_access_token.ex b/test/support/lib/dummy/oauth_access_tokens/oauth_access_token.ex index f2e59941..c9ec8e32 100644 --- a/test/support/lib/dummy/oauth_access_tokens/oauth_access_token.ex +++ b/test/support/lib/dummy/oauth_access_tokens/oauth_access_token.ex @@ -13,4 +13,19 @@ defmodule Dummy.OauthAccessTokens.OauthAccessToken do access_token_fields() timestamps() end + + @impl ExOauth2Provider.Changeset + def allowed_fields do + access_token_allowed_fields() + end + + @impl ExOauth2Provider.Changeset + def required_fields do + access_token_required_fields() + end + + @impl ExOauth2Provider.Changeset + def request_fields do + access_token_request_fields() + end end