diff --git a/.formatter.exs b/.formatter.exs index dc40b68..28a219c 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -64,6 +64,8 @@ spark_locals_without_parens = [ password_field: 1, password_reset_action_name: 1, private_key: 1, + private_key_id: 1, + private_key_path: 1, read_action_name: 1, read_expired_action_name: 1, redirect_uri: 1, @@ -92,6 +94,7 @@ spark_locals_without_parens = [ store_token_action_name: 1, strategy_attribute_name: 1, subject_name: 1, + team_id: 1, token_lifetime: 1, token_param_name: 1, token_resource: 1, diff --git a/lib/ash_authentication.ex b/lib/ash_authentication.ex index 48adff8..e0c2cac 100644 --- a/lib/ash_authentication.ex +++ b/lib/ash_authentication.ex @@ -115,6 +115,7 @@ defmodule AshAuthentication do AshAuthentication.Strategy.Auth0, AshAuthentication.Strategy.Github, AshAuthentication.Strategy.Google, + AshAuthentication.Strategy.Apple, AshAuthentication.Strategy.MagicLink, AshAuthentication.Strategy.OAuth2, AshAuthentication.Strategy.Oidc, diff --git a/lib/ash_authentication/strategies/apple.ex b/lib/ash_authentication/strategies/apple.ex new file mode 100644 index 0000000..198aaf8 --- /dev/null +++ b/lib/ash_authentication/strategies/apple.ex @@ -0,0 +1,29 @@ +defmodule AshAuthentication.Strategy.Apple do + alias __MODULE__.{Dsl, Verifier} + + @moduledoc """ + Strategy for authenticating using [Apple Sign In](https://developer.apple.com/sign-in-with-apple/) + + This strategy builds on-top of `AshAuthentication.Strategy.Oidc` and + [`assent`](https://hex.pm/packages/assent). + + In order to use Apple Sign In you need to provide the following minimum configuration: + + - `client_id` + - `team_id` + - `private_key_id` + - `private_key_path` + - `redirect_uri` + + ## More documentation: + - The [Apple Sign In Documentation](https://developer.apple.com/documentation/sign_in_with_apple). + - The [OIDC documentation](`AshAuthentication.Strategy.Oidc`) + """ + + alias AshAuthentication.Strategy.{Custom, Oidc} + + use Custom, entity: Dsl.dsl() + + defdelegate transform(strategy, dsl_state), to: Oidc + defdelegate verify(strategy, dsl_state), to: Verifier +end diff --git a/lib/ash_authentication/strategies/apple/dsl.ex b/lib/ash_authentication/strategies/apple/dsl.ex new file mode 100644 index 0000000..a823bfb --- /dev/null +++ b/lib/ash_authentication/strategies/apple/dsl.ex @@ -0,0 +1,95 @@ +defmodule AshAuthentication.Strategy.Apple.Dsl do + @moduledoc false + + alias AshAuthentication.Strategy.{Custom, Oidc} + + @doc false + @spec dsl :: Custom.entity() + def dsl do + secret_type = AshAuthentication.Dsl.secret_type() + + Oidc.Dsl.dsl() + |> Map.merge(%{ + name: :apple, + args: [{:optional, :name, :apple}], + describe: """ + Provides a pre-configured authentication strategy for [Apple Sign In](https://developer.apple.com/sign-in-with-apple/). + + This strategy is built using the `:oidc` strategy, and thus provides all the same + configuration options should you need them. + + ## More documentation: + - The [Apple Sign In Documentation](https://developer.apple.com/documentation/sign_in_with_apple). + - The [OIDC documentation](`AshAuthentication.Strategy.Oidc`) + + #### Strategy defaults: + + #{strategy_override_docs(Assent.Strategy.Apple)} + """, + auto_set_fields: strategy_fields(Assent.Strategy.Apple, icon: :apple), + schema: patch_schema(secret_type) + }) + end + + defp patch_schema(secret_type) do + Oidc.Dsl.dsl() + |> Map.get(:schema, []) + |> Keyword.merge( + team_id: [ + type: secret_type, + doc: "The Apple team ID associated with the application.", + required: true + ], + private_key_id: [ + type: secret_type, + doc: "The private key ID used for signing the JWT token.", + required: true + ], + private_key_path: [ + type: secret_type, + doc: "The path to the private key file used for signing the JWT token.", + required: true + ] + ) + end + + defp strategy_fields(strategy, params) do + strategy.default_config([]) + |> Enum.map(fn + {:client_authentication_method, method} -> + {:client_authentication_method, String.to_existing_atom(method)} + + {:openid_configuration, config} -> + {:openid_configuration, atomize_keys(config)} + + {key, value} -> + {key, value} + end) + |> Keyword.put(:assent_strategy, strategy) + |> Keyword.merge(params) + end + + # sobelow_skip ["DOS.StringToAtom"] + defp atomize_keys(map) do + map + |> Enum.map(fn {key, value} -> {String.to_atom(key), value} end) + |> Enum.into(%{}) + end + + defp strategy_override_docs(strategy) do + defaults = + strategy.default_config([]) + |> Enum.map_join( + ".\n", + fn {key, value} -> + " * `#{inspect(key)}` is set to `#{inspect(value)}`" + end + ) + + """ + The following defaults are applied: + + #{defaults}. + """ + end +end diff --git a/lib/ash_authentication/strategies/apple/verifier.ex b/lib/ash_authentication/strategies/apple/verifier.ex new file mode 100644 index 0000000..6f61c7d --- /dev/null +++ b/lib/ash_authentication/strategies/apple/verifier.ex @@ -0,0 +1,19 @@ +defmodule AshAuthentication.Strategy.Apple.Verifier do + @moduledoc """ + DSL verifier for Apple strategy. + """ + + alias AshAuthentication.Strategy.OAuth2 + import AshAuthentication.Validations + + @doc false + @spec verify(OAuth2.t(), map) :: :ok | {:error, Exception.t()} + def verify(strategy, _dsl_state) do + with :ok <- validate_secret(strategy, :client_id), + :ok <- validate_secret(strategy, :team_id), + :ok <- validate_secret(strategy, :private_key_id), + :ok <- validate_secret(strategy, :private_key_path) do + validate_secret(strategy, :redirect_uri) + end + end +end diff --git a/lib/ash_authentication/strategies/oauth2.ex b/lib/ash_authentication/strategies/oauth2.ex index 91ae015..a78136f 100644 --- a/lib/ash_authentication/strategies/oauth2.ex +++ b/lib/ash_authentication/strategies/oauth2.ex @@ -232,6 +232,8 @@ defmodule AshAuthentication.Strategy.OAuth2 do openid_configuration_uri: nil, openid_configuration: nil, private_key: nil, + private_key_id: nil, + private_key_path: nil, provider: :oauth2, redirect_uri: nil, register_action_name: nil, @@ -240,6 +242,7 @@ defmodule AshAuthentication.Strategy.OAuth2 do sign_in_action_name: nil, site: nil, strategy_module: __MODULE__, + team_id: nil, token_url: nil, trusted_audiences: nil, user_url: nil @@ -280,6 +283,8 @@ defmodule AshAuthentication.Strategy.OAuth2 do openid_configuration_uri: nil | binary, openid_configuration: nil | map, private_key: secret, + private_key_id: secret, + private_key_path: secret, provider: atom, redirect_uri: secret, register_action_name: atom, @@ -288,6 +293,7 @@ defmodule AshAuthentication.Strategy.OAuth2 do sign_in_action_name: atom, site: secret, strategy_module: module, + team_id: secret, token_url: secret, trusted_audiences: secret_list, user_url: secret diff --git a/lib/ash_authentication/strategies/oauth2/plug.ex b/lib/ash_authentication/strategies/oauth2/plug.ex index ee141a0..ad63be6 100644 --- a/lib/ash_authentication/strategies/oauth2/plug.ex +++ b/lib/ash_authentication/strategies/oauth2/plug.ex @@ -88,6 +88,27 @@ defmodule AshAuthentication.Strategy.OAuth2.Plug do {:ok, config} <- add_secret_value(config, strategy, :client_id, !!strategy.base_url), {:ok, config} <- add_secret_value(config, strategy, :client_secret, !!strategy.base_url), {:ok, config} <- add_secret_value(config, strategy, :token_url, !!strategy.base_url), + {:ok, config} <- + add_secret_value( + config, + strategy, + :team_id, + strategy.assent_strategy != Assent.Strategy.Apple + ), + {:ok, config} <- + add_secret_value( + config, + strategy, + :private_key_id, + strategy.assent_strategy != Assent.Strategy.Apple + ), + {:ok, config} <- + add_secret_value( + config, + strategy, + :private_key_path, + strategy.assent_strategy != Assent.Strategy.Apple + ), {:ok, config} <- add_secret_value(config, strategy, :trusted_audiences, true), {:ok, config} <- add_http_adapter(config),