Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Freeze httpoison jason issue #45 #48

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,4 @@ _build
cover/

.vscode/
doc
49 changes: 48 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<div align="center">

# `elixir-auth-microsoft`
<h1>elixir-auth-microsoft</h1>

The _easy_ way to add **Microsoft `OAuth` authentication**
to your **`Elixir` / `Phoenix`** app.
Expand All @@ -19,6 +19,29 @@ Just plug-and-play in **5 mins**.

</div>

- [_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
Expand Down Expand Up @@ -403,6 +426,30 @@ Thank you! 🙏

<br />


## 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
Expand Down
52 changes: 29 additions & 23 deletions demo/lib/app_web/httpoison_mock.ex
Original file line number Diff line number Diff line change
@@ -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: "[email protected]"}
)
}}
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: "[email protected]"}
)
}}
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
42 changes: 42 additions & 0 deletions demo/lib/app_web/microsoft_auth.ex
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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 = %{
Expand All @@ -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 = %{
Expand All @@ -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"]

Expand All @@ -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"]

Expand All @@ -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)
Expand Down
6 changes: 4 additions & 2 deletions demo/mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
10 changes: 5 additions & 5 deletions lib/elixir_auth_microsoft.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
Expand All @@ -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

Expand All @@ -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}"
Expand Down
2 changes: 1 addition & 1 deletion lib/httpoison_mock.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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]},
Expand Down
2 changes: 1 addition & 1 deletion mix.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
Expand Down
6 changes: 3 additions & 3 deletions test/elixir_auth_microsoft_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
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
Expand All @@ -39,14 +39,14 @@
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

# Tests the generated OAuth URL of logout
test "generate_oauth_url_logout() for dev/localhost " do
conn = %{

Check warning on line 49 in test/elixir_auth_microsoft_test.exs

View workflow job for this annotation

GitHub Actions / Build and test

variable "conn" is unused (if the variable is not meant to be used, prefix it with an underscore)
host: "localhost",
port: 4000
}
Expand All @@ -59,7 +59,7 @@
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
Expand Down
Loading