pow-auth / assent

Multi-provider framework in Elixir
https://powauth.com
MIT License
391 stars 45 forks source link

Error with multitenant Azure login #37

Closed gregoribic closed 4 years ago

gregoribic commented 4 years ago

[error] #PID<0.888.0> running TaskAppWeb.Endpoint (connection #PID<0.828.0>, stream id 27) terminated Server: localhost:4006 (https) Request: POST /auth/azure/callback (exit) an exception was raised: (RuntimeError) Invalid issuer "https://login.microsoftonline.com/270a4662-e407-4044-b299-1a62945d3893/v2.0" in ID Token (pow_assent 0.4.6) lib/pow_assent/phoenix/controllers/authorization_controller.ex:209: PowAssent.Phoenix.AuthorizationController.handle_strategy_error/1 (pow_assent 0.4.6) lib/pow_assent/phoenix/controllers/authorization_controller.ex:1: PowAssent.Phoenix.AuthorizationController.action/2 (pow_assent 0.4.6) lib/pow_assent/phoenix/controllers/authorization_controller.ex:1: PowAssent.Phoenix.AuthorizationController.phoenix_controller_pipeline/2 (phoenix 1.4.16) lib/phoenix/router.ex:288: Phoenix.Router.call/2 (task_app 0.1.0) lib/task_app_web/endpoint.ex:1: TaskAppWeb.Endpoint.plug_builder_call/2 (task_app 0.1.0) lib/plug/debugger.ex:132: TaskAppWeb.Endpoint."call (overridable 3)"/2 (task_app 0.1.0) lib/task_app_web/endpoint.ex:1: TaskAppWeb.Endpoint.call/2 (phoenix 1.4.16) lib/phoenix/endpoint/cowboy2_handler.ex:42: Phoenix.Endpoint.Cowboy2Handler.init/4

danschultzer commented 4 years ago

Thanks, sorry for the delay, been super busy with work.

So continuing from the elixirforum this is due to how OpenID specs works. There are some options you can try out.

Use OAuth2 base strategy

Instead of depending on OpenID, you could use the OAuth 2.0 base strategy with like this (same as what the ueberauth strategy does):

[
  client_id: "REPLACE_WITH_CLIENT_ID",
  client_secret: "REPLACE_WITH_CLIENT_SECRET",
  auth_method: :client_secret_post,
  site: "https://graph.microsoft.com",
  authorize_url: "https://login.microsoftonline.com/#{tenant_id}/oauth2/v2.0/authorize",
  token_url: "https://login.microsoftonline.com/#{tenant_id}/oauth2/v2.0/token",
  authorization_params: [scope: "https://graph.microsoft.com/user.read openid email offline_access"],
  user_url: "https://graph.microsoft.com/v1.0/me/"
]

Not sure if this works out of the box, but it would be pretty easy to set it up as a custom strategy to parse the right user struct.

Dynamically set the tenant id

You may be able to continue using OIDC if you can fetch the tenant id from the returned auth token. This would require you to add a plug or similar, and then dynamically update the config like this: https://github.com/pow-auth/pow_assent/issues/117#issuecomment-571837184

danschultzer commented 4 years ago

I assume this has been resolved 🙂

gregoribic commented 4 years ago

I did not managed to get it working without tenant id.

danschultzer commented 4 years ago

With the new v0.1.12 release of assent, you will be able to customize the get_user callback. You can either bypass the validation entirely or dynamically update the issuer in the config.

The latter could look something like this (untested):

defmodule MyAppWeb.AzureADStrategy do
  @moduledoc false
  use Assent.Strategy.OIDC.Base

  alias Assent.{Config, Strategy.OIDC}

  @impl true
  def default_config(config) do
    [
      site: "https://login.microsoftonline.com/common/v2.0",
      authorization_params: [scope: "email profile", response_mode: "form_post"],
      client_auth_method: :client_secret_post,
    ]
  end

  @impl true
  def normalize(_config, user), do: {:ok, user}

  @impl true
  def get_user(config, token) do
    with {:ok, issuer} <- fetch_iss(token["id_token"], config),
         {:ok, config} <- update_issuer_in_config(config, issuer),
         {:ok, jwt}    <- OIDC.validate_id_token(config, token["id_token"]) do
      Helpers.normalize_userinfo(jwt.claims)
    end
  end

  defp fetch_iss(encoded, config) do
    with [_, encoded, _] <- String.split(encoded, "."),
         {:ok, json}     <- Base.url_decode64(encoded, padding: false),
         {:ok, claims}   <- Config.json_library(config).decode(json) do
      Map.fetch(claims, "iss")
    else
      {:error, error} -> {:error, error}
      _any            -> {:error, "The ID Token is not a valid JWT"}
    end
  end

  defp update_issuer_in_config(config, issuer) do
    openid_configuration = Map.put(config[:openid_configuration], "issuer", issuer)

    {:ok, Map.put(config, :openid_configuration, openid_configuration)}
  end
end
vince-roy commented 3 years ago

In case it might help anyone, this version based on what Dan kindly provided above worked for me:

defmodule MyApp.Assent.AzureADCommonStrategy do
  @moduledoc false
  use Assent.Strategy.OIDC.Base

  alias Assent.{Config, Strategy.OIDC}

  @impl true
  def default_config(config) do
    [
      authorization_params: [scope: "email profile", response_mode: "form_post"],
      client_auth_method: :client_secret_post,
      site: "https://login.microsoftonline.com/common/v2.0"
    ]
  end

  @impl true
  def normalize(_config, user), do: {:ok, user}

  @impl true
  def fetch_user(config, token) do
    with {:ok, issuer} <- fetch_iss(token["id_token"], config),
         {:ok, config} <- update_issuer_in_config(config, issuer),
         {:ok, jwt}    <- OIDC.validate_id_token(config, token["id_token"]) do
      Helpers.normalize_userinfo(jwt.claims)
    end
  end

  defp fetch_iss(encoded, config) do
    with [_, encoded, _] <- String.split(encoded, "."),
         {:ok, json}     <- Base.url_decode64(encoded, padding: false),
         {:ok, claims}   <- Config.json_library(config).decode(json) do
      Map.fetch(claims, "iss")
    else
      {:error, error} -> {:error, error}
      _any            -> {:error, "The ID Token is not a valid JWT"}
    end
  end

  defp update_issuer_in_config(config, issuer) do
    openid_configuration = Map.put(config[:openid_configuration], "issuer", issuer)
    {:ok, Keyword.put(config, :openid_configuration, openid_configuration)}
  end
end