diff --git a/.gitignore b/.gitignore
index 48408db..8411289 100644
--- a/.gitignore
+++ b/.gitignore
@@ -38,3 +38,4 @@ _build
cover/
.vscode/
+doc
diff --git a/README.md b/README.md
index 1447bc4..e1dbe61 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
-# `elixir-auth-microsoft`
+
elixir-auth-microsoft
The _easy_ way to add **Microsoft `OAuth` authentication**
to your **`Elixir` / `Phoenix`** app.
@@ -19,6 +19,29 @@ Just plug-and-play in **5 mins**.
+- [_Why_? 🤷](#why-)
+- [_What_? 💭](#what-)
+- [_Who_? 👥](#who-)
+- [_How_? ✅](#how-)
+ - [1. Add the hex package to `deps` 📦](#1-add-the-hex-package-to-deps-)
+ - [2. Create an App Registration in Azure Active Directory 🆕](#2-create-an-app-registration-in-azure-active-directory-)
+ - [3. Export Environment / Application Variables](#3-export-environment--application-variables)
+ - [A note on tenants](#a-note-on-tenants)
+ - [4. Add a "Sign in with Microsoft" Button to your App](#4-add-a-sign-in-with-microsoft-button-to-your-app)
+ - [5. Use the Built-in Functions to Authenticate People :shipit:](#5-use-the-built-in-functions-to-authenticate-people-shipit)
+ - [6. Add the `/auth/microsoft/callback` to `router.ex`](#6-add-the-authmicrosoftcallback-to-routerex)
+ - [6.1 Give it a try!](#61-give-it-a-try)
+ - [7. Logging the person out](#7-logging-the-person-out)
+ - [7.1 Setting up the post-logout redirect URI](#71-setting-up-the-post-logout-redirect-uri)
+ - [7.2 Add button for person to log out](#72-add-button-for-person-to-log-out)
+ - [_Done_!](#done)
+ - [Testing](#testing)
+ - [Complete Working Demo / Example `Phoenix` App 🚀](#complete-working-demo--example-phoenix-app-)
+ - [Optimised SVG + CSS Button](#optimised-svg--css-button)
+ - [Notes 📝](#notes-)
+ - [Branding Guidelines](#branding-guidelines)
+
+
# _Why_? 🤷
Following
@@ -403,6 +426,30 @@ Thank you! 🙏
+
+## Testing
+
+If you want pre-defined responses without making real requests
+when testing,
+you can add the following property `httpoison_mock`
+in your `test.exs` configuration file.
+
+```elixir
+config :elixir_auth_microsoft,
+ httpoison_mock: true
+```
+
+With this setting turned on,
+calls will return successful requests
+with mock data.
+
+Of course, you could always a mocking library like
+[`mox`](https://github.com/dashbitco/mox)
+for this.
+But if you want a quick way to test your app with this package,
+this option may be for you!
+
+
## Complete Working Demo / Example `Phoenix` App 🚀
If you get stuck
diff --git a/demo/lib/app_web/httpoison_mock.ex b/demo/lib/app_web/httpoison_mock.ex
index e5574b7..0003c0f 100644
--- a/demo/lib/app_web/httpoison_mock.ex
+++ b/demo/lib/app_web/httpoison_mock.ex
@@ -1,30 +1,36 @@
defmodule ElixirAuthMicrosoft.HTTPoisonMock do
- def get("https://login.microsoftonline.com/common/oauth2/v2.0/authorize") do
- {:error, :bad_request}
- end
+ @moduledoc """
+ SHOULD BE THE SAME AS THE ORIGINAL `httpoison_mock.ex`.
+ """
+ @spec get(any, nonempty_maybe_improper_list) :: {:error, :bad_request} | {:ok, %{body: binary}}
+ def get(_url, [ {:Authorization, token} = _authorization | _content_type]) do
+ is_token_valid = token !== "Bearer invalid_token"
- def get(_url, _headers) do
- {:ok,
- %{
- body:
- Jason.encode!(%{
- businessPhones: [],
- displayName: "Test Name",
- givenName: "Test",
- id: "192jnsd9010apd",
- jobTitle: nil,
- mail: nil,
- mobilePhone: '+351928837834',
- officeLocation: nil,
- preferredLanguage: nil,
- surname: "Name",
- userPrincipalName: "testemail@hotmail.com"}
- )
- }}
+ if is_token_valid do
+ {:ok,
+ %{
+ body:
+ Jason.encode!(%{
+ businessPhones: [],
+ displayName: "Test Name",
+ givenName: "Test",
+ id: "192jnsd9010apd",
+ jobTitle: nil,
+ mail: nil,
+ mobilePhone: '+351928837834',
+ officeLocation: nil,
+ preferredLanguage: nil,
+ surname: "Name",
+ userPrincipalName: "testemail@hotmail.com"}
+ )
+ }}
+ else
+ {:error, :bad_request}
+ end
end
-
+ @spec post(any, any, any) :: {:ok, %{body: binary}}
def post(_url, _body, _headers) do
- {:ok, %{body: Jason.encode!(%{access_token: "token1", code: "code1"})}}
+ {:ok, %{body: Jason.encode!(%{access_token: "token1"})}}
end
end
diff --git a/demo/lib/app_web/microsoft_auth.ex b/demo/lib/app_web/microsoft_auth.ex
index 11472ab..4689bb3 100644
--- a/demo/lib/app_web/microsoft_auth.ex
+++ b/demo/lib/app_web/microsoft_auth.ex
@@ -1,4 +1,8 @@
defmodule ElixirAuthMicrosoft do
+ @moduledoc """
+ SHOULD BE THE SAME AS THE ORIGINAL `microsoft_auth.ex`.
+ """
+
@default_authorize_url "https://login.microsoftonline.com/common/oauth2/v2.0/authorize"
@default_logout_url "https://login.microsoftonline.com/common/oauth2/v2.0/logout"
@default_token_url "https://login.microsoftonline.com/common/oauth2/v2.0/token"
@@ -8,8 +12,18 @@ defmodule ElixirAuthMicrosoft do
@httpoison (Application.compile_env(:elixir_auth_microsoft, :httpoison_mock) && ElixirAuthMicrosoft.HTTPoisonMock) || HTTPoison
+ @doc """
+ `http/0` injects a TestDouble in test envs.
+ When testing, it uses a mocked version of HTTPoison with predictible results. When in production, it uses the original version.
+ """
def http, do: @httpoison
+ @doc """
+ `generate_oauth_url_authorize/1` creates an OAuth2 URL with client_id, redirect_uri and scopes (be sure to create the app registration in Azure Portal AD).
+ The redirect_uri will be the URL Microsoft will redirect after successful sign-in.
+ This is the URL that you should be used in a "Login with Microsoft"-type button.
+ """
+ @spec generate_oauth_url_authorize(Conn.t()) :: String.t()
def generate_oauth_url_authorize(conn) do
query = %{
@@ -24,11 +38,24 @@ defmodule ElixirAuthMicrosoft do
"#{microsoft_authorize_url()}?{params}"
end
+ @doc """
+ `generate_oauth_url_authorize/2` is the same as `generate_oauth_url_authorize/1` but with a state parameter.
+ This state parameter should be compared with the one that is sent as query param in the redirect URI after the sign-in is successful.
+ """
+ @spec generate_oauth_url_authorize(%{:host => any, optional(any) => any}, binary) :: String.t()
def generate_oauth_url_authorize(conn, state) when is_binary(state) do
params = URI.encode_query(%{state: state}, :rfc3986)
generate_oauth_url_authorize(conn) <> "{params}"
end
+
+ @doc """
+ `generate_oauth_url_logout/0` creates a logout URL.
+ This should the URL the person is redirected to when they want to logout.
+ To define the redirect URL (the URL that the user will be redirected to after successful logout from Microsoft ),
+ you need to set the `MICROSOFT_POST_LOGOUT_REDIRECT_URI` env variable
+ or `:post_logout_redirect_uri` in the config file.
+ """
def generate_oauth_url_logout() do
query = %{
@@ -39,6 +66,11 @@ defmodule ElixirAuthMicrosoft do
"#{microsoft_logout_url()}?{params}"
end
+ @doc """
+ `get_token/2` fetches the ID token using the authorization code that was previously obtained.
+ Env variables are used to encode information while fetching the ID token from Microsoft, including the registered client ID that was created in Azure Portal AD.
+ """
+ @spec get_token(String.t(), Conn.t()) :: {:ok, map} | {:error, any}
def get_token(code, conn) do
headers = ["Content-Type": "multipart/form-data"]
@@ -57,6 +89,10 @@ defmodule ElixirAuthMicrosoft do
end
+ @doc """
+ `get_user_profile/1` fetches the signed-in Microsoft User info according to the token that is passed by calling `get_token/1`.
+ """
+ @spec get_user_profile(String.t()) :: {:error, any} | {:ok, map}
def get_user_profile(token) do
headers = ["Authorization": "Bearer #{token}", "Content-Type": "application/json"]
@@ -65,7 +101,13 @@ defmodule ElixirAuthMicrosoft do
end
+ @doc """
+ `parse_body_response/1` parses the response from Microsoft's endpoints.
+ The keys of the decoded map are converted in atoms, for easier access in templates.
+ ##TODO check cases where the parsed code when fetching fails.
+ """
+ @spec parse_body_response({atom, String.t()} | {:error, any}) :: {:ok, map} | {:error, any}
def parse_body_response({:error, err}), do: {:error, err}
def parse_body_response({:ok, response}) do
body = Map.get(response, :body)
diff --git a/demo/mix.exs b/demo/mix.exs
index 9691bcc..556f5e3 100644
--- a/demo/mix.exs
+++ b/demo/mix.exs
@@ -44,9 +44,11 @@ defmodule App.MixProject do
{:telemetry_metrics, "~> 0.6"},
{:telemetry_poller, "~> 1.0"},
{:gettext, "~> 0.18"},
- {:jason, "~> 1.2"},
{:plug_cowboy, "~> 2.5"},
- {:httpoison, "~> 1.8.0"},
+
+ # Dependencies used by the package (to be the same as the package, make sure it's the same in the root mix.exs)
+ {:httpoison, ">= 0.6.1"},
+ {:jason, ">= 1.0.0"},
]
end
diff --git a/lib/elixir_auth_microsoft.ex b/lib/elixir_auth_microsoft.ex
index dfb25f1..80b0012 100644
--- a/lib/elixir_auth_microsoft.ex
+++ b/lib/elixir_auth_microsoft.ex
@@ -28,13 +28,13 @@ defmodule ElixirAuthMicrosoft do
@spec generate_oauth_url_authorize(Conn.t()) :: String.t()
def generate_oauth_url_authorize(conn) do
- query = %{
+ query = [
client_id: microsoft_client_id(),
response_type: "code",
redirect_uri: generate_redirect_uri(conn),
scope: get_microsoft_scopes(),
response_mode: "query"
- }
+ ]
params = URI.encode_query(query, :rfc3986)
"#{microsoft_authorize_url()}?{params}"
@@ -46,7 +46,7 @@ defmodule ElixirAuthMicrosoft do
"""
@spec generate_oauth_url_authorize(%{:host => any, optional(any) => any}, binary) :: String.t()
def generate_oauth_url_authorize(conn, state) when is_binary(state) do
- params = URI.encode_query(%{state: state}, :rfc3986)
+ params = URI.encode_query([state: state], :rfc3986)
generate_oauth_url_authorize(conn) <> "{params}"
end
@@ -60,9 +60,9 @@ defmodule ElixirAuthMicrosoft do
"""
def generate_oauth_url_logout() do
- query = %{
+ query = [
post_logout_redirect_uri: microsoft_post_logout_redirect_uri(),
- }
+ ]
params = URI.encode_query(query, :rfc3986)
"#{microsoft_logout_url()}?{params}"
diff --git a/lib/httpoison_mock.ex b/lib/httpoison_mock.ex
index a508ac5..5ebc451 100644
--- a/lib/httpoison_mock.ex
+++ b/lib/httpoison_mock.ex
@@ -38,7 +38,7 @@ defmodule ElixirAuthMicrosoft.HTTPoisonMock do
@doc """
Mocks the `post/3` function from HTTPoison.
- It yields a predictable, with always the same access token.
+ It yields a predictable result, with always the same access token.
"""
@spec post(any, any, any) :: {:ok, %{body: binary}}
def post(_url, _body, _headers) do
diff --git a/mix.exs b/mix.exs
index 2226d30..89203b2 100644
--- a/mix.exs
+++ b/mix.exs
@@ -33,8 +33,8 @@ defmodule ElixirAuthMicrosoft.MixProject do
# Run "mix help deps" to learn about dependencies.
defp deps do
[
- {:httpoison, "~> 1.8.0"},
- {:jason, "~> 1.2"},
+ {:httpoison, ">= 0.6.1"},
+ {:jason, ">= 1.0.0"},
# Testing
{:excoveralls, "~> 0.18.0", only: [:test, :dev]},
diff --git a/mix.lock b/mix.lock
index bb7d337..d3c6cd6 100644
--- a/mix.lock
+++ b/mix.lock
@@ -9,7 +9,7 @@
"excoveralls": {:hex, :excoveralls, "0.18.0", "b92497e69465dc51bc37a6422226ee690ab437e4c06877e836f1c18daeb35da9", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "1109bb911f3cb583401760be49c02cbbd16aed66ea9509fc5479335d284da60b"},
"file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"},
"hackney": {:hex, :hackney, "1.20.1", "8d97aec62ddddd757d128bfd1df6c5861093419f8f7a4223823537bad5d064e2", [:rebar3], [{:certifi, "~> 2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "fe9094e5f1a2a2c0a7d10918fee36bfec0ec2a979994cff8cfe8058cd9af38e3"},
- "httpoison": {:hex, :httpoison, "1.8.2", "9eb9c63ae289296a544842ef816a85d881d4a31f518a0fec089aaa744beae290", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "2bb350d26972e30c96e2ca74a1aaf8293d61d0742ff17f01e0279fef11599921"},
+ "httpoison": {:hex, :httpoison, "2.2.1", "87b7ed6d95db0389f7df02779644171d7319d319178f6680438167d7b69b1f3d", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "51364e6d2f429d80e14fe4b5f8e39719cacd03eb3f9a9286e61e216feac2d2df"},
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
"jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"},
"makeup": {:hex, :makeup, "1.1.1", "fa0bc768698053b2b3869fa8a62616501ff9d11a562f3ce39580d60860c3a55e", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "5dc62fbdd0de44de194898b6710692490be74baa02d9d108bc29f007783b0b48"},
diff --git a/test/elixir_auth_microsoft_test.exs b/test/elixir_auth_microsoft_test.exs
index e97d26f..dbafbeb 100644
--- a/test/elixir_auth_microsoft_test.exs
+++ b/test/elixir_auth_microsoft_test.exs
@@ -19,7 +19,7 @@ defmodule ElixirAuthMicrosoftTest do
expected_scope = URI.encode_www_form("https://graph.microsoft.com/User.Read")
expected =
- "https://login.microsoftonline.com/common/oauth2/v2.0/authorize?&client_id=" <> id <> "&redirect_uri=" <> expected_redirect_uri <> "&response_mode=query&response_type=code&scope=" <> expected_scope <> "&state=" <> state
+ "https://login.microsoftonline.com/common/oauth2/v2.0/authorize?&client_id=#{id}&response_type=code&redirect_uri=#{expected_redirect_uri}&scope=#{expected_scope}&response_mode=query&state=#{state}"
assert ElixirAuthMicrosoft.generate_oauth_url_authorize(conn, state) == expected
end
@@ -39,7 +39,7 @@ defmodule ElixirAuthMicrosoftTest do
expected_scope = URI.encode_www_form("https://graph.microsoft.com/User.Read")
expected =
- "https://login.microsoftonline.com/common/oauth2/v2.0/authorize?&client_id=" <> id <> "&redirect_uri=" <> expected_redirect_uri <> "&response_mode=query&response_type=code&scope=" <> expected_scope
+ "https://login.microsoftonline.com/common/oauth2/v2.0/authorize?&client_id=#{id}&response_type=code&redirect_uri=#{expected_redirect_uri}&scope=#{expected_scope}&response_mode=query"
assert ElixirAuthMicrosoft.generate_oauth_url_authorize(conn) == expected
end
@@ -59,7 +59,7 @@ defmodule ElixirAuthMicrosoftTest do
expected_redirect_uri = URI.encode_www_form("http://localhost:4000/auth/microsoft/logout")
expected =
- "https://login.microsoftonline.com/common/oauth2/v2.0/logout?&post_logout_redirect_uri=" <> expected_redirect_uri
+ "https://login.microsoftonline.com/common/oauth2/v2.0/logout?&post_logout_redirect_uri=#{expected_redirect_uri}"
assert ElixirAuthMicrosoft.generate_oauth_url_logout() == expected
end