diff --git a/.formatter.exs b/.formatter.exs index 62badd99..b628f2e1 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -100,6 +100,9 @@ spark_locals_without_parens = [ magic_link: 1, magic_link: 2, max_size: 1, + microsoft: 0, + microsoft: 1, + microsoft: 2, monitor_fields: 1, multitenancy_relationship: 1, name: 1, diff --git a/documentation/dsls/DSL-AshAuthentication.Strategy.Microsoft.md b/documentation/dsls/DSL-AshAuthentication.Strategy.Microsoft.md new file mode 100644 index 00000000..36e48089 --- /dev/null +++ b/documentation/dsls/DSL-AshAuthentication.Strategy.Microsoft.md @@ -0,0 +1,117 @@ + +# AshAuthentication.Strategy.Microsoft + +Strategy for authenticating using [Microsoft](https://microsoft.com) + +This strategy builds on-top of `AshAuthentication.Strategy.Oidc` and +[`assent`](https://hex.pm/packages/assent). + +It uses Microsoft's OpenID Connect discovery endpoint to automatically +retrieve token, authorization, and user info URLs. User identity claims +(email, name, etc.) are extracted from the ID token returned during the +authorization code flow. + +In order to use Microsoft you need to provide the following minimum configuration: + + - `client_id` + - `redirect_uri` + - `client_secret` + +By default the strategy uses the `common` tenant endpoint, which allows any +Microsoft account (personal, work, or school). Multi-tenant issuer validation +is handled automatically — the `{tenantid}` template in Microsoft's discovery +document is resolved from the ID token's `tid` claim before validation. + +To restrict sign-in to a specific Azure tenant, override `base_url`: + + base_url "https://login.microsoftonline.com/YOUR_TENANT_ID/v2.0" + +## More documentation: +- The [Microsoft OpenID Connect Overview](https://learn.microsoft.com/en-us/entra/identity-platform/v2-protocols-oidc). +- The [Microsoft Tutorial](/documentation/tutorials/microsoft.md) +- The [OIDC documentation](`AshAuthentication.Strategy.Oidc`) + + + +### authentication.strategies.microsoft +```elixir +microsoft name \\ :microsoft +``` + + +Provides a pre-configured authentication strategy for [Microsoft](https://microsoft.com/). + +This strategy is built using the `:oidc` strategy, and automatically +retrieves configuration from Microsoft's discovery endpoint +(`https://login.microsoftonline.com/{tenant|common}/v2.0/.well-known/openid-configuration`). + +By default the strategy uses the `common` tenant endpoint. To restrict +sign-in to a specific Azure tenant, override `base_url`: + +base_url "https://login.microsoftonline.com/YOUR_TENANT_ID/v2.0" + +###### More documentation: +- The [Microsoft OpenID Connect Overview](https://learn.microsoft.com/en-us/entra/identity-platform/v2-protocols-oidc) +- The [Microsoft Tutorial](/documentation/tutorials/microsoft.md) +- The [OIDC documentation](`AshAuthentication.Strategy.Oidc`) + +###### Strategy defaults: + +The following defaults are applied: + +* `:base_url` is set to `"https://login.microsoftonline.com/common/v2.0"`. +* `:authorization_params` is set to `[scope: "email profile"]`. +* `:client_authentication_method` is set to `"client_secret_post"`. + + + + + + + +### Arguments + +| Name | Type | Default | Docs | +|------|------|---------|------| +| [`name`](#authentication-strategies-microsoft-name){: #authentication-strategies-microsoft-name .spark-required} | `atom` | | Uniquely identifies the strategy. | +### Options + +| Name | Type | Default | Docs | +|------|------|---------|------| +| [`client_id`](#authentication-strategies-microsoft-client_id){: #authentication-strategies-microsoft-client_id .spark-required} | `(any, any -> any) \| module \| String.t` | | The OAuth2 client ID. Takes either a module which implements the `AshAuthentication.Secret` behaviour, a 2 arity anonymous function or a string. | +| [`redirect_uri`](#authentication-strategies-microsoft-redirect_uri){: #authentication-strategies-microsoft-redirect_uri .spark-required} | `(any, any -> any) \| module \| String.t` | | The callback URI *base*. Not the whole URI back to the callback endpoint, but the URI to your `AuthPlug`. Takes either a module which implements the `AshAuthentication.Secret` behaviour, a 2 arity anonymous function or a string. | +| [`base_url`](#authentication-strategies-microsoft-base_url){: #authentication-strategies-microsoft-base_url } | `(any, any -> any) \| module \| String.t` | `"https://login.microsoftonline.com/common/v2.0"` | The base URL of the OAuth2 server - including the leading protocol (ie `https://`). Takes either a module which implements the `AshAuthentication.Secret` behaviour, a 2 arity anonymous function or a string. | +| [`site`](#authentication-strategies-microsoft-site){: #authentication-strategies-microsoft-site } | `(any, any -> any) \| module \| String.t` | | Deprecated: Use `base_url` instead. | +| [`prevent_hijacking?`](#authentication-strategies-microsoft-prevent_hijacking?){: #authentication-strategies-microsoft-prevent_hijacking? } | `boolean` | `true` | Requires a confirmation add_on to be present if the password strategy is used with the same identity_field. | +| [`auth_method`](#authentication-strategies-microsoft-auth_method){: #authentication-strategies-microsoft-auth_method } | `nil \| :client_secret_basic \| :client_secret_post \| :client_secret_jwt \| :private_key_jwt` | `:client_secret_post` | The authentication strategy used, optional. If not set, no authentication will be used during the access token request. | +| [`client_secret`](#authentication-strategies-microsoft-client_secret){: #authentication-strategies-microsoft-client_secret } | `(any, any -> any) \| module \| String.t` | | The OAuth2 client secret. Required if :auth_method is `:client_secret_basic`, `:client_secret_post` or `:client_secret_jwt`. Takes either a module which implements the `AshAuthentication.Secret` behaviour, a 2 arity anonymous function or a string. | +| [`trusted_audiences`](#authentication-strategies-microsoft-trusted_audiences){: #authentication-strategies-microsoft-trusted_audiences } | `(any, any -> any) \| module \| list(any) \| nil` | | A list of audiences which are trusted. Takes either a module which implements the `AshAuthentication.Secret` behaviour, a 2 arity anonymous function or a string. | +| [`private_key`](#authentication-strategies-microsoft-private_key){: #authentication-strategies-microsoft-private_key } | `(any, any -> any) \| module \| String.t` | | The private key to use if `:auth_method` is `:private_key_jwt`. Takes either a module which implements the `AshAuthentication.Secret` behaviour, a 2 arity anonymous function or a string. | +| [`code_verifier`](#authentication-strategies-microsoft-code_verifier){: #authentication-strategies-microsoft-code_verifier } | `boolean` | `false` | Boolean to generate and use a random 128 byte long url safe code verifier for PKCE flow, optional, defaults to false. When set to true the session params will contain :code_verifier, :code_challenge, and :code_challenge_method params | +| [`authorization_params`](#authentication-strategies-microsoft-authorization_params){: #authentication-strategies-microsoft-authorization_params } | `(any, any -> any) \| module \| keyword \| nil` | `[scope: "email profile"]` | Any additional parameters to encode in the request phase. eg: `authorization_params scope: "openid profile email"` | +| [`registration_enabled?`](#authentication-strategies-microsoft-registration_enabled?){: #authentication-strategies-microsoft-registration_enabled? } | `boolean` | `true` | If enabled, new users will be able to register for your site when authenticating and not already present. If not, only existing users will be able to authenticate. | +| [`register_action_name`](#authentication-strategies-microsoft-register_action_name){: #authentication-strategies-microsoft-register_action_name } | `atom` | | The name of the action to use to register a user, if `registration_enabled?` is `true`. Defaults to `register_with_` See the "Registration and Sign-in" section of the strategy docs for more. | +| [`sign_in_action_name`](#authentication-strategies-microsoft-sign_in_action_name){: #authentication-strategies-microsoft-sign_in_action_name } | `atom` | | The name of the action to use to sign in an existing user, if `sign_in_enabled?` is `true`. Defaults to `sign_in_with_`, which is generated for you by default. See the "Registration and Sign-in" section of the strategy docs for more information. | +| [`identity_resource`](#authentication-strategies-microsoft-identity_resource){: #authentication-strategies-microsoft-identity_resource } | `module \| false` | `false` | The resource used to store user identities, or `false` to disable. See the User Identities section of the strategy docs for more. | +| [`identity_relationship_name`](#authentication-strategies-microsoft-identity_relationship_name){: #authentication-strategies-microsoft-identity_relationship_name } | `atom` | `:identities` | Name of the relationship to the provider identities resource | +| [`identity_relationship_user_id_attribute`](#authentication-strategies-microsoft-identity_relationship_user_id_attribute){: #authentication-strategies-microsoft-identity_relationship_user_id_attribute } | `atom` | `:user_id` | The name of the destination (user_id) attribute on your provider identity resource. Only necessary if you've changed the `user_id_attribute_name` option of the provider identity. | +| [`openid_configuration_uri`](#authentication-strategies-microsoft-openid_configuration_uri){: #authentication-strategies-microsoft-openid_configuration_uri } | `(any, any -> any) \| module \| String.t` | `"/.well-known/openid-configuration"` | The URI for the OpenID provider | +| [`client_authentication_method`](#authentication-strategies-microsoft-client_authentication_method){: #authentication-strategies-microsoft-client_authentication_method } | `"client_secret_basic" \| "client_secret_post" \| "client_secret_jwt" \| "private_key_jwt" \| "none"` | `"client_secret_post"` | The client authentication method to use. | +| [`openid_configuration`](#authentication-strategies-microsoft-openid_configuration){: #authentication-strategies-microsoft-openid_configuration } | `nil \| %{optional(String.t) => any}` | | The OpenID configuration. If not set, the configuration will be retrieved from `openid_configuration_uri`. | +| [`id_token_signed_response_alg`](#authentication-strategies-microsoft-id_token_signed_response_alg){: #authentication-strategies-microsoft-id_token_signed_response_alg } | `"HS256" \| "HS384" \| "HS512" \| "RS256" \| "RS384" \| "RS512" \| "ES256" \| "ES384" \| "ES512" \| "PS256" \| "PS384" \| "PS512" \| "Ed25519" \| "Ed25519ph" \| "Ed448" \| "Ed448ph" \| "EdDSA"` | `"RS256"` | The `id_token_signed_response_alg` parameter sent by the Client during Registration. | +| [`id_token_ttl_seconds`](#authentication-strategies-microsoft-id_token_ttl_seconds){: #authentication-strategies-microsoft-id_token_ttl_seconds } | `nil \| pos_integer` | | The number of seconds from `iat` that an ID Token will be considered valid. | +| [`nonce`](#authentication-strategies-microsoft-nonce){: #authentication-strategies-microsoft-nonce } | `boolean \| (any, any -> any) \| module \| String.t` | `true` | A function for generating the session nonce, `true` to automatically generate it with `AshAuthentication.Strategy.Oidc.NonceGenerator`, or `false` to disable. | + + + + + +### Introspection + +Target: `AshAuthentication.Strategy.OAuth2` + + + + diff --git a/documentation/dsls/DSL-AshAuthentication.Strategy.Microsoft.md.license b/documentation/dsls/DSL-AshAuthentication.Strategy.Microsoft.md.license new file mode 100644 index 00000000..f66b5ef4 --- /dev/null +++ b/documentation/dsls/DSL-AshAuthentication.Strategy.Microsoft.md.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2022 Alembic Pty Ltd + +SPDX-License-Identifier: MIT diff --git a/documentation/dsls/DSL-AshAuthentication.md b/documentation/dsls/DSL-AshAuthentication.md index f0d4848a..68c5f483 100644 --- a/documentation/dsls/DSL-AshAuthentication.md +++ b/documentation/dsls/DSL-AshAuthentication.md @@ -65,6 +65,7 @@ Currently supported strategies: - `AshAuthentication.Strategy.Auth0` - `AshAuthentication.Strategy.Github` - `AshAuthentication.Strategy.Google` + - `AshAuthentication.Strategy.Microsoft` - `AshAuthentication.Strategy.Oidc` - `AshAuthentication.Strategy.Slack` 3. `AshAuthentication.Strategy.MagicLink` diff --git a/documentation/tutorials/microsoft.md b/documentation/tutorials/microsoft.md new file mode 100644 index 00000000..ecf51cd1 --- /dev/null +++ b/documentation/tutorials/microsoft.md @@ -0,0 +1,117 @@ + + +# Microsoft Tutorial + +This is a quick tutorial on how to configure Microsoft (Azure AD) authentication. + +First you'll need a registered application in the [Microsoft Entra admin center](https://entra.microsoft.com/), in order to get your OAuth 2.0 credentials. + +1. Under the **Entra ID** fan click **App registrations** +2. Click **New registration** +3. Enter a name for your application +4. Under **Redirect URI**, select **Web** and enter your callback URL. E.g. `http://localhost:4000/auth/user/microsoft/callback` +5. Click **Register** +6. From the app's **Overview** page, copy the **Application (client) ID** — this is your `client_id` +7. From the same **Overview** page, copy the **Directory (tenant) ID** — you'll need this if you want to restrict sign-in to a specific tenant +8. Navigate to **Certificates & secrets** > **+ New client secret**, add a description and expiry, then copy the secret **Value** — this is your `client_secret` + +Next we configure our resource to use Microsoft credentials: + +```elixir +defmodule MyApp.Accounts.User do + use Ash.Resource, + extensions: [AshAuthentication], + domain: MyApp.Accounts + + attributes do + ... + end + + authentication do + strategies do + microsoft do + client_id MyApp.Secrets + redirect_uri MyApp.Secrets + client_secret MyApp.Secrets + end + end + end +end +``` + +By default the strategy uses the `common` tenant endpoint, which allows any Microsoft +account (personal, work, or school). To restrict sign-in to a specific Azure tenant, +override `base_url`: + +```elixir +microsoft do + client_id MyApp.Secrets + redirect_uri MyApp.Secrets + client_secret MyApp.Secrets + base_url "https://login.microsoftonline.com/YOUR_TENANT_ID/v2.0" +end +``` + +Please check the [guide](https://hexdocs.pm/ash_authentication/AshAuthentication.Secret.html) on how to properly configure your Secrets. +Then we need to define an action that will handle the oauth2 flow, for the Microsoft case it is `:register_with_microsoft` — it will handle both cases for our resource, user registration & login. + +```elixir +defmodule MyApp.Accounts.User do + require Ash.Resource.Change.Builtins + use Ash.Resource, + extensions: [AshAuthentication], + domain: MyApp.Accounts + + # ... + actions do + create :register_with_microsoft do + argument :user_info, :map, allow_nil?: false + argument :oauth_tokens, :map, allow_nil?: false + upsert? true + upsert_identity :unique_email + + change AshAuthentication.GenerateTokenChange + + # Required if you have the `identity_resource` configuration enabled. + change AshAuthentication.Strategy.OAuth2.IdentityChange + + change fn changeset, _ -> + user_info = Ash.Changeset.get_argument(changeset, :user_info) + + Ash.Changeset.change_attributes(changeset, Map.take(user_info, ["email"])) + end + + # Required if you're using the password & confirmation strategies + upsert_fields [] + change set_attribute(:confirmed_at, &DateTime.utc_now/0) + end + end + + # ... + +end +``` + +Ensure you set the `hashed_password` to `allow_nil?` if you are also using the password strategy. + +```elixir +defmodule MyApp.Accounts.User do + # ... + attributes do + # ... + attribute :hashed_password, :string, allow_nil?: true, sensitive?: true + end + # ... +end +``` + +And generate and run migrations in that case. + +```bash +mix ash.codegen make_hashed_password_nullable +mix ash.migrate +``` diff --git a/lib/ash_authentication.ex b/lib/ash_authentication.ex index 62f8db59..ea9b1cd5 100644 --- a/lib/ash_authentication.ex +++ b/lib/ash_authentication.ex @@ -68,6 +68,7 @@ defmodule AshAuthentication do - `AshAuthentication.Strategy.Auth0` - `AshAuthentication.Strategy.Github` - `AshAuthentication.Strategy.Google` + - `AshAuthentication.Strategy.Microsoft` - `AshAuthentication.Strategy.Oidc` - `AshAuthentication.Strategy.Slack` 3. `AshAuthentication.Strategy.MagicLink` @@ -146,6 +147,7 @@ defmodule AshAuthentication do AshAuthentication.Strategy.Github, AshAuthentication.Strategy.Google, AshAuthentication.Strategy.MagicLink, + AshAuthentication.Strategy.Microsoft, AshAuthentication.Strategy.OAuth2, AshAuthentication.Strategy.Oidc, AshAuthentication.Strategy.Password, diff --git a/lib/ash_authentication/strategies/microsoft.ex b/lib/ash_authentication/strategies/microsoft.ex new file mode 100644 index 00000000..24ec22c7 --- /dev/null +++ b/lib/ash_authentication/strategies/microsoft.ex @@ -0,0 +1,46 @@ +# SPDX-FileCopyrightText: 2022 Alembic Pty Ltd +# +# SPDX-License-Identifier: MIT + +defmodule AshAuthentication.Strategy.Microsoft do + alias __MODULE__.Dsl + + @moduledoc """ + Strategy for authenticating using [Microsoft](https://microsoft.com) + + This strategy builds on-top of `AshAuthentication.Strategy.Oidc` and + [`assent`](https://hex.pm/packages/assent). + + It uses Microsoft's OpenID Connect discovery endpoint to automatically + retrieve token, authorization, and user info URLs. User identity claims + (email, name, etc.) are extracted from the ID token returned during the + authorization code flow. + + In order to use Microsoft you need to provide the following minimum configuration: + + - `client_id` + - `redirect_uri` + - `client_secret` + + By default the strategy uses the `common` tenant endpoint, which allows any + Microsoft account (personal, work, or school). Multi-tenant issuer validation + is handled automatically — the `{tenantid}` template in Microsoft's discovery + document is resolved from the ID token's `tid` claim before validation. + + To restrict sign-in to a specific Azure tenant, override `base_url`: + + base_url "https://login.microsoftonline.com/YOUR_TENANT_ID/v2.0" + + ## More documentation: + - The [Microsoft OpenID Connect Overview](https://learn.microsoft.com/en-us/entra/identity-platform/v2-protocols-oidc). + - The [Microsoft Tutorial](/documentation/tutorials/microsoft.md) + - 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: Oidc +end diff --git a/lib/ash_authentication/strategies/microsoft/azure_ad_multitenant.ex b/lib/ash_authentication/strategies/microsoft/azure_ad_multitenant.ex new file mode 100644 index 00000000..455c5e74 --- /dev/null +++ b/lib/ash_authentication/strategies/microsoft/azure_ad_multitenant.ex @@ -0,0 +1,57 @@ +# SPDX-FileCopyrightText: 2022 Alembic Pty Ltd +# +# SPDX-License-Identifier: MIT + +defmodule AshAuthentication.Strategy.Microsoft.AzureADMultitenant do + @moduledoc """ + When using Microsoft's `/common` or `/organizations` OIDC endpoints, the + discovery document returns a templated issuer: + + https://login.microsoftonline.com/{tenantid}/v2.0 + + The actual ID token's `iss` claim contains the real tenant ID and is + patched in via this module. + """ + + use Assent.Strategy.OIDC.Base + alias Assent.Strategy.{AzureAD, OIDC} + + @impl true + def default_config(config) do + config + |> AzureAD.default_config() + |> Keyword.update(:authorization_params, [], fn params -> + # Remove `form_post` inherited from Assent's AzureAD defaults + # to avoid CSRF issues. + Keyword.delete(params, :response_mode) + end) + end + + @impl true + def fetch_user(config, token) do + config + |> patch_issuer(token) + |> OIDC.fetch_user(token) + end + + defp patch_issuer(config, %{"id_token" => id_token}) do + with %{"issuer" => issuer} = openid_config <- Keyword.get(config, :openid_configuration), + true <- String.contains?(issuer, "{tenantid}"), + {:ok, tenant_id} <- tenant_id(id_token) do + patched = Map.put(openid_config, "issuer", String.replace(issuer, "{tenantid}", tenant_id)) + Keyword.put(config, :openid_configuration, patched) + else + _ -> config + end + end + + defp patch_issuer(config, _token), do: config + + defp tenant_id(id_token) do + with [_, payload, _] <- String.split(id_token, "."), + {:ok, json} <- Base.url_decode64(payload, padding: false), + {:ok, %{"tid" => tid}} <- Jason.decode(json) do + {:ok, tid} + end + end +end diff --git a/lib/ash_authentication/strategies/microsoft/dsl.ex b/lib/ash_authentication/strategies/microsoft/dsl.ex new file mode 100644 index 00000000..a8097576 --- /dev/null +++ b/lib/ash_authentication/strategies/microsoft/dsl.ex @@ -0,0 +1,61 @@ +# SPDX-FileCopyrightText: 2022 Alembic Pty Ltd +# +# SPDX-License-Identifier: MIT + +defmodule AshAuthentication.Strategy.Microsoft.Dsl do + @moduledoc false + + alias AshAuthentication.Strategy.{Custom, Oidc} + alias AshAuthentication.Strategy.Microsoft.AzureADMultitenant + + @doc false + @spec dsl :: Custom.entity() + def dsl do + Oidc.dsl() + |> Map.merge(%{ + name: :microsoft, + args: [{:optional, :name, :microsoft}], + describe: """ + Provides a pre-configured authentication strategy for [Microsoft](https://microsoft.com/). + + This strategy is built using the `:oidc` strategy, and automatically + retrieves configuration from Microsoft's discovery endpoint + (`https://login.microsoftonline.com/{tenant|common}/v2.0/.well-known/openid-configuration`). + + By default the strategy uses the `common` tenant endpoint. To restrict + sign-in to a specific Azure tenant, override `base_url`: + + base_url "https://login.microsoftonline.com/YOUR_TENANT_ID/v2.0" + + #### More documentation: + - The [Microsoft OpenID Connect Overview](https://learn.microsoft.com/en-us/entra/identity-platform/v2-protocols-oidc) + - The [Microsoft Tutorial](/documentation/tutorials/microsoft.md) + - The [OIDC documentation](`AshAuthentication.Strategy.Oidc`) + + #### Strategy defaults: + + #{strategy_override_docs(AzureADMultitenant)} + """, + auto_set_fields: [icon: :microsoft, assent_strategy: AzureADMultitenant] + }) + |> Custom.set_defaults(AzureADMultitenant.default_config([])) + 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/mix.exs b/mix.exs index 580e43c5..50a29e40 100644 --- a/mix.exs +++ b/mix.exs @@ -98,6 +98,8 @@ defmodule AshAuthentication.MixProject do search_data: Spark.Docs.search_data_for(AshAuthentication.Strategy.Google)}, {"documentation/dsls/DSL-AshAuthentication.Strategy.MagicLink.md", search_data: Spark.Docs.search_data_for(AshAuthentication.Strategy.MagicLink)}, + {"documentation/dsls/DSL-AshAuthentication.Strategy.Microsoft.md", + search_data: Spark.Docs.search_data_for(AshAuthentication.Strategy.Microsoft)}, {"documentation/dsls/DSL-AshAuthentication.Strategy.OAuth2.md", search_data: Spark.Docs.search_data_for(AshAuthentication.Strategy.OAuth2)}, {"documentation/dsls/DSL-AshAuthentication.Strategy.Oidc.md", @@ -124,6 +126,7 @@ defmodule AshAuthentication.MixProject do "documentation/tutorials/github.md", "documentation/tutorials/google.md", "documentation/tutorials/magic-links.md", + "documentation/tutorials/microsoft.md", "documentation/tutorials/password.md", "documentation/tutorials/slack.md", "documentation/tutorials/totp.md" @@ -178,6 +181,7 @@ defmodule AshAuthentication.MixProject do AshAuthentication.Strategy.Custom, AshAuthentication.Strategy.Github, AshAuthentication.Strategy.Google, + AshAuthentication.Strategy.Microsoft, AshAuthentication.Strategy.MagicLink, AshAuthentication.Strategy.OAuth2, AshAuthentication.Strategy.Oidc, @@ -271,6 +275,7 @@ defmodule AshAuthentication.MixProject do "AshAuthentication.Strategy.Github", "AshAuthentication.Strategy.Google", "AshAuthentication.Strategy.MagicLink", + "AshAuthentication.Strategy.Microsoft", "AshAuthentication.Strategy.OAuth2", "AshAuthentication.Strategy.Oidc", "AshAuthentication.Strategy.Password",