defmodule AzureADOpenId do
@moduledoc """
Azure Active Directory authentication using OpenID.
"""
alias AzureADOpenId.Strategy
alias AzureADOpenId.Verify
@type uri :: String.t()
@type config_values :: {:tenant, String.t()} | {:client_id, String.t()}
@type config :: nil | [config_values]
@type id_token :: map()
@type callback_response :: {:ok, id_token} | {:error, String.t(), String.t()}
# Plug.Conn.t
@type conn :: map()
@doc """
Get an access token using the client credentials authorisation strategy for
machine to machine authentication. Requires a client secret.
"""
def get_access_token!(config \\ nil) do
config = config || get_config()
Strategy.ClientCredentials.get_token!(config)
end
@doc """
Verify an access token.
"""
def verify_access_token!(access_token, config \\ nil) do
config = config || get_config()
Verify.Token.access_token!(access_token, config)
end
@doc """
Get a redirect url for authorization using Azure Active Directory login.
"""
@spec authorize_url!(uri, config) :: uri
def authorize_url!(redirect_uri, config \\ nil) do
config = config || get_config()
Strategy.AuthCode.authorize_url!(redirect_uri, config)
end
@doc """
Handles and validates the `t:id_token/0` in the callback response. The redirect_uri used in the
`authorize_url!/1` function should redirect to a path that uses this funtion.
"""
@spec handle_callback!(conn, config) :: callback_response
def handle_callback!(conn, config \\ nil) do
config = config || get_config()
case Map.get(conn, :params) do
%{"id_token" => id_token, "code" => code} ->
verify_id_token(id_token, code, config)
%{"error" => error, "error_description" => error_description} ->
{:error, error, error_description}
_ ->
{:error, "missing_code_or_token", "Missing code or id_token"}
end
end
defp verify_id_token(id_token, code, config) do
try do
claims = Verify.Token.id_token!(id_token, code, config)
{:ok, claims}
rescue
error in RuntimeError ->
{:error, "failed_auth_callback", error.message}
end
end
@doc """
Returns the redirect url for logging out of Azure Active Directory.
"""
@spec logout_url(uri) :: uri
def logout_url(redirect_uri) when is_binary(redirect_uri) do
logout_url(get_config(), redirect_uri)
end
@doc """
Returns the redirect url for logging out of Azure Active Directory.
"""
@spec logout_url(config, uri | nil) :: uri
def logout_url(config \\ nil, redirect_uri \\ nil) do
config = config || get_config()
tenant = config[:tenant]
client_id = config[:client_id]
url = "https://login.microsoftonline.com/#{tenant}/oauth2/logout?client_id=#{client_id}"
if is_binary(redirect_uri) do
redirect_uri = URI.encode_www_form(redirect_uri)
url <> "?post_logout_redirect_uri=" <> redirect_uri
else
url
end
end
@doc """
Checks if the library is configured with the standard Elixir configuration (i.e. using
the config files).
"""
@spec configured?() :: boolean()
def configured?() do
configset = get_config()
configset != nil &&
Keyword.has_key?(configset, :tenant) &&
Keyword.has_key?(configset, :client_id)
end
defp get_config() do
Application.get_env(:azure_ad_openid, AzureADOpenId)
end
@doc """
Returns a human readable user name from an `t:id_token/0`. This is useful as the
Azure Active Directory `t:id_token/0` can be very inconsistent in how user names are stored.
"""
@spec get_user_name(id_token) :: String.t()
def get_user_name(token) do
cond do
token["family_name"] && token["given_name"] ->
name = token["given_name"] <> " " <> token["family_name"]
format_name(name)
token["upn"] ->
format_name(token["upn"])
token["name"] ->
format_name(token["name"])
token["email"] ->
format_name(token["email"])
true ->
"No Name"
end
end
defp format_name(name) do
if should_format(name) do
name
|> String.split(["@", "_"])
|> hd
|> String.split([".", " "])
|> Enum.map(&capitalize/1)
|> Enum.join(" ")
else
name
end
end
defp capitalize(name) do
if has_upper?(name) do
name
else
String.capitalize(name)
end
end
@valid_upper String.graphemes("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
defp has_upper?(name), do: String.contains?(name, @valid_upper)
defp should_format(name) do
cond do
String.contains?(name, [".", "@", "_"]) ->
true
has_upper?(name) ->
false
true ->
true
end
end
end