pow-auth / pow_assent

Multi-provider authentication for your Pow enabled app
https://powauth.com
MIT License
318 stars 49 forks source link

Implement strategy for Azure Active Directory (Azure AD) #1

Closed leifericf closed 5 years ago

leifericf commented 5 years ago

It would be useful to have support for Azure AD out-of-the-box. Something along the lines of that provided by ueberauth_microsoft, but for Pow instead.

I might implement this myself and submit a pull request, once my skills are sufficient. Opening a feature request here to flag the need and in case somebody else decides to pick it up in the meantime.

danschultzer commented 5 years ago

I would appreciate that! I'm not familiar with Azure AD, but looks like it can be done using the OAuth 2.0 protocol: https://github.com/KonaTeam/omniauth-azure-oauth2

If you just need to use OAuth 2.0, then it'll take very little time to implement as PowAssent already have that strategy built-in. Take a look at the Google OAuth2 integration:

https://github.com/danschultzer/pow_assent/blob/master/lib/pow_assent/strategies/google.ex https://github.com/danschultzer/pow_assent/blob/master/test/pow_assent/strategies/google_test.exs

You'll mostly just need to update URL's, and add the right user profile API response for testing that the information can be normalized to something useful for PowAssent.

From this guide, I see that a JWT can be used instead of a client secret for added security. That would require a bit more code in the callback phase similar to how the facebook integration modifies the callback flow to add in app proof: https://github.com/danschultzer/pow_assent/blob/master/lib/pow_assent/strategies/facebook.ex#L19-L34

I would be happy to help out making the integration work If you create a PR. I can work on the parts that requires some more code like the certificate and response handling (from what I can read it seems that also the API response will be encoded as a JWT).

leifericf commented 5 years ago

Thanks a bunch for the pointers and offering to help! I think you're right about being able to use OAuth 2.0. I'll look into that first. Unfortunately, the concepts of authentication and authorisation (except simple username/password setups) are quite foreign to me still, so it will take some time.

danschultzer commented 5 years ago

I've only tested used the provided information in the Azure AD OAuth 2.0 tutorial, but this hopefully works: https://github.com/danschultzer/pow_assent/pull/3

You can read about tenant id configuration here: https://github.com/danschultzer/pow_assent/blob/bbb500d7134b50a369226c663953932703767a4d/lib/pow_assent/strategies/azure_oauth2.ex#L2-L30

I don't have an azure account myself, so I'll need to set up one later to test it when I got some free time.

leifericf commented 5 years ago

Awesome, thanks! I'm still struggling with more basics of Elixir and Phoenix at the moment, but I'll definitely give this a whirl at the office.

leifericf commented 5 years ago

I've tried this out today. Think I've got PowAssent configured correctly for Azure AD.

Gave this a whirl today and encountered this error after setting up:

[error] #PID<0.429.0> running AzureAuthWeb.Endpoint (cowboy_protocol) terminated
Server: localhost:4000 (http)
Request: GET /registration/new
** (exit) an exception was raised:
    ** (ArgumentError) unknown field `email`. Only fields, embeds and associations (except :through ones) are supported in changesets
        (ecto) lib/ecto/changeset.ex:489: Ecto.Changeset.type!/2
        (ecto) lib/ecto/changeset.ex:464: Ecto.Changeset.process_param/7
        (elixir) lib/enum.ex:1925: Enum."-reduce/3-lists^foldl/2-0-"/3
        (ecto) lib/ecto/changeset.ex:449: Ecto.Changeset.cast/6
        (pow) lib/pow/ecto/schema/changeset.ex:43: Pow.Ecto.Schema.Changeset.user_id_field_changeset/3
        (azure_auth) lib/azure_auth/users/user.ex:3: AzureAuth.Users.User.pow_changeset/2
        (pow) lib/pow/phoenix/controllers/registration_controller.ex:15: Pow.Phoenix.RegistrationController.process_new/2
        (pow) lib/pow/phoenix/controllers/controller.ex:87: Pow.Phoenix.Controller.action/3
        (pow) lib/pow/phoenix/controllers/registration_controller.ex:1: Pow.Phoenix.RegistrationController.action/2
        (pow) lib/pow/phoenix/controllers/registration_controller.ex:1: Pow.Phoenix.RegistrationController.phoenix_controller_pipeline/2
        (azure_auth) lib/azure_auth_web/endpoint.ex:1: AzureAuthWeb.Endpoint.instrument/4
        (phoenix) lib/phoenix/router.ex:278: Phoenix.Router.__call__/1
        (azure_auth) lib/azure_auth_web/endpoint.ex:1: AzureAuthWeb.Endpoint.plug_builder_call/2
        (azure_auth) lib/plug/debugger.ex:122: AzureAuthWeb.Endpoint."call (overridable 3)"/2
        (azure_auth) lib/azure_auth_web/endpoint.ex:1: AzureAuthWeb.Endpoint.call/2
        (plug) lib/plug/adapters/cowboy/handler.ex:16: Plug.Adapters.Cowboy.Handler.upgrade/4
        (cowboy) /Users/leif/Code/azure_auth/deps/cowboy/src/cowboy_protocol.erl:442: :cowboy_protocol.execute/4

Currently trying to figure out why that error occurs.

danschultzer commented 5 years ago

Can you post the content of your user.ex module? It must be missing the email field.

danschultzer commented 5 years ago

Oh, and the migration file for your user table.

leifericf commented 5 years ago

My user.ex looks like this:

defmodule AzureAuth.Users.User do
  use Ecto.Schema
  use Pow.Ecto.Schema
  use PowAssent.Ecto.Schema

  schema "users" do
    has_many :user_identities,
      AzureAuth.UserIdentities.UserIdentity,
      on_delete: :delete_all
  end
end

And the migration file for the user table looks like this (as it was generated by Pow):

defmodule AzureAuth.Repo.Migrations.CreateUsers do
  use Ecto.Migration

  def change do
    create table(:users) do
      add :email, :string, null: false
      add :password_hash, :string

      timestamps()
    end

    create unique_index(:users, [:email])
  end
end
danschultzer commented 5 years ago

Update your user schema to this:

defmodule AzureAuth.Users.User do
  use Ecto.Schema
  use Pow.Ecto.Schema
  use PowAssent.Ecto.Schema

  schema "users" do
    has_many :user_identities,
      AzureAuth.UserIdentities.UserIdentity,
      on_delete: :delete_all

    pow_user_fields()

    timestamps()
  end
end
leifericf commented 5 years ago

Aha! Of course, silly me. I removed pow_user_fields() and timestamps().

Now it works!

danschultzer commented 5 years ago

Great, I've updated the readme to make it clear that those two fields should stay 😄

leifericf commented 5 years ago

Hmmm. Encountering a different error when visiting http://localhost:4000/auth/azure/new/:

[error] #PID<0.525.0> running AzureAuthWeb.Endpoint (cowboy_protocol) terminated
Server: localhost:4000 (http)
Request: GET /auth/azure/new/
** (exit) an exception was raised:
    ** (UndefinedFunctionError) function PowAssent.Strategy.AzureOAuth2.authorize_url/2 is undefined (module PowAssent.Strategy.AzureOAuth2 is not available)
        PowAssent.Strategy.AzureOAuth2.authorize_url([redirect_uri: "http://localhost:4000/auth/azure/callback", client_id: "my_client_id", client_secret: "my_client_secret", strategy: PowAssent.Strategy.AzureOAuth2], %Plug.Conn{adapter: {Plug.Adapters.Cowboy.Conn, :...}, assigns: %{callback_url: "http://localhost:4000/auth/azure/callback", current_user: nil}, before_send: [#Function<0.116269836/1 in Plug.CSRFProtection.call/2>, #Function<4.101542213/1 in Phoenix.Controller.fetch_flash/2>, #Function<0.58261320/1 in Plug.Session.before_send/2>, #Function<1.25166163/1 in Plug.Logger.call/2>, #Function<0.61641163/1 in Phoenix.LiveReloader.before_send_inject_reloader/2>], body_params: %{}, cookies: %{"_azure_auth_key" => "my_azure_auth_key"}, halted: false, host: "localhost", method: "GET", owner: #PID<0.525.0>, params: %{"provider" => "azure"}, path_info: ["auth", "azure", "new"], path_params: %{"provider" => "azure"}, peer: {{127, 0, 0, 1}, 57299}, port: 4000, private: %{AzureAuthWeb.Router => {[], %{}}, :phoenix_action => :new, :phoenix_controller => PowAssent.Phoenix.AuthorizationController, :phoenix_endpoint => AzureAuthWeb.Endpoint, :phoenix_flash => %{}, :phoenix_format => "html", :phoenix_layout => {AzureAuthWeb.LayoutView, :app}, :phoenix_pipelines => [:browser], :phoenix_router => AzureAuthWeb.Router, :phoenix_view => PowAssent.Phoenix.AuthorizationView, :plug_session => %{"_csrf_token" => "my_token"}, :plug_session_fetch => :done, :pow_config => [mod: Pow.Plug.Session, otp_app: :azure_auth]}, query_params: %{}, query_string: "", remote_ip: {127, 0, 0, 1}, req_cookies: %{"_azure_auth_key" => "my_azure_auth_key"}, req_headers: [{"host", "localhost:4000"}, {"accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"}, {"upgrade-insecure-requests", "1"}, {"cookie", "_azure_auth_key=my_azure_auth_key"}, {"user-agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/11.1.2 Safari/605.1.15"}, {"accept-language", "en-us"}, {"accept-encoding", "gzip, deflate"}, {"connection", "keep-alive"}], request_path: "/auth/azure/new/", resp_body: nil, resp_cookies: %{}, resp_headers: [{"cache-control", "max-age=0, private, must-revalidate"}, {"x-frame-options", "SAMEORIGIN"}, {"x-xss-protection", "1; mode=block"}, {"x-content-type-options", "nosniff"}, {"x-download-options", "noopen"}, {"x-permitted-cross-domain-policies", "none"}], scheme: :http, script_name: [], secret_key_base: "my_secret_key_base", state: :unset, status: nil})
        (pow_assent) lib/pow_assent/plug.ex:20: PowAssent.Plug.authenticate/3
        (pow) lib/pow/phoenix/controllers/controller.ex:87: Pow.Phoenix.Controller.action/3
        (pow_assent) lib/pow_assent/phoenix/controllers/authorization_controller.ex:1: PowAssent.Phoenix.AuthorizationController.action/2
        (pow_assent) lib/pow_assent/phoenix/controllers/authorization_controller.ex:1: PowAssent.Phoenix.AuthorizationController.phoenix_controller_pipeline/2
        (azure_auth) lib/azure_auth_web/endpoint.ex:1: AzureAuthWeb.Endpoint.instrument/4
        (phoenix) lib/phoenix/router.ex:278: Phoenix.Router.__call__/1
        (azure_auth) lib/azure_auth_web/endpoint.ex:1: AzureAuthWeb.Endpoint.plug_builder_call/2
        (azure_auth) lib/plug/debugger.ex:122: AzureAuthWeb.Endpoint."call (overridable 3)"/2
        (azure_auth) lib/azure_auth_web/endpoint.ex:1: AzureAuthWeb.Endpoint.call/2
        (plug) lib/plug/adapters/cowboy/handler.ex:16: Plug.Adapters.Cowboy.Handler.upgrade/4
        (cowboy) /Users/leif/Code/azure_auth/deps/cowboy/src/cowboy_protocol.erl:442: :cowboy_protocol.execute/4
danschultzer commented 5 years ago

You'll have to use the azure oauth2 github branch in your mix dependencies:

{:pow_assent, git: "https://github.com/danschultzer/pow_assent.git", branch: "azure-oauth2-strategy"}

I'll merge it in once I can confirm it works.

leifericf commented 5 years ago

Aha! I see. Switched branch and got one step further, now this error is thrown:

[info] GET /auth/azure/new/
[debug] Processing with PowAssent.Phoenix.AuthorizationController.new/2
  Parameters: %{"provider" => "azure"}
  Pipelines: [:browser]
[info] Sent 500 in 74ms
[error] #PID<0.3887.0> running AzureAuthWeb.Endpoint (cowboy_protocol) terminated
Server: localhost:4000 (http)
Request: GET /auth/azure/new/
** (exit) an exception was raised:
    ** (FunctionClauseError) no function clause matching in Keyword.get/3
        (elixir) lib/keyword.ex:188: Keyword.get(%OAuth2.Client{authorize_url: "/common/oauth2/authorize", client_id: "my_client_id", client_secret: "my_client_secret", headers: [], params: %{}, redirect_uri: "http://localhost:4000/auth/azure/callback", ref: nil, request_opts: [], site: "https://login.microsoftonline.com", strategy: PowAssent.Strategy.AzureOAuth2, token: nil, token_method: :post, token_url: "/common/oauth2/token"}, :tenant_id, "common")
        (pow_assent) lib/pow_assent/strategies/azure_oauth2.ex:46: PowAssent.Strategy.AzureOAuth2.default_config/1
        (pow_assent) lib/pow_assent/strategies/azure_oauth2.ex:42: PowAssent.Strategy.AzureOAuth2.set_config/1
        (pow_assent) lib/pow_assent/strategies/azure_oauth2.ex:42: PowAssent.Strategy.AzureOAuth2.authorize_url/2
        (oauth2) lib/oauth2/client.ex:188: OAuth2.Client.authorize_url/2
        (oauth2) lib/oauth2/client.ex:201: OAuth2.Client.authorize_url!/2
        (pow_assent) lib/pow_assent/strategies/oauth2.ex:36: PowAssent.Strategy.OAuth2.authorize_url/2
        (pow_assent) lib/pow_assent/plug.ex:20: PowAssent.Plug.authenticate/3
        (pow) lib/pow/phoenix/controllers/controller.ex:87: Pow.Phoenix.Controller.action/3
        (pow_assent) lib/pow_assent/phoenix/controllers/authorization_controller.ex:1: PowAssent.Phoenix.AuthorizationController.action/2
        (pow_assent) lib/pow_assent/phoenix/controllers/authorization_controller.ex:1: PowAssent.Phoenix.AuthorizationController.phoenix_controller_pipeline/2
        (azure_auth) lib/azure_auth_web/endpoint.ex:1: AzureAuthWeb.Endpoint.instrument/4
        (phoenix) lib/phoenix/router.ex:278: Phoenix.Router.__call__/1
        (azure_auth) lib/azure_auth_web/endpoint.ex:1: AzureAuthWeb.Endpoint.plug_builder_call/2
        (azure_auth) lib/plug/debugger.ex:122: AzureAuthWeb.Endpoint."call (overridable 3)"/2
        (azure_auth) lib/azure_auth_web/endpoint.ex:1: AzureAuthWeb.Endpoint.call/2
        (plug) lib/plug/adapters/cowboy/handler.ex:16: Plug.Adapters.Cowboy.Handler.upgrade/4
        (cowboy) /Users/leif/Code/azure_auth/deps/cowboy/src/cowboy_protocol.erl:442: :cowboy_protocol.execute/4
danschultzer commented 5 years ago

Sorry, I introduced an error in the last commits. Try run mix deps.update pow_assent to update PowAssent, and see if it doesn't work now!

leifericf commented 5 years ago

Nice! One step further. I feel like we're getting close!

Now I get redirected to Microsoft's page, and it shows this error:

Request Id: my_request_id
Correlation Id: my_correlation_id
Timestamp: 2018-09-10T09:56:24Z
Message: AADSTS50011: The reply url specified in the request does not match the reply urls configured for the application: 'my_application_id'.

I suspect this is because the callback URL is not correctly set in Azure AD on my end.

I have asked our resident Azure AD expert to set this callback URL for testing: http://localhost:4000/auth/azure/callback

Will continue testing once that is done 👍

leifericf commented 5 years ago

I think we've managed to fix the callback URL, and now it seems like I'm getting some kind of response. But now it's complaining about Cross-Site Request Forgery (CSRF):

[info] GET /auth/azure/new/
[debug] Processing with PowAssent.Phoenix.AuthorizationController.new/2
  Parameters: %{"provider" => "azure"}
  Pipelines: [:browser]
[info] Sent 302 in 63ms
[info] GET /auth/azure/callback
[debug] Processing with PowAssent.Phoenix.AuthorizationController.callback/2
  Parameters: %{"code" => "LONG_CODE", "provider" => "azure", "session_state" => "SESSION_STATE", "state" => "STATE"}
  Pipelines: [:browser]
[info] Sent 500 in 828ms
[error] #PID<0.428.0> running MyAppWeb.Endpoint (cowboy_protocol) terminated
Server: localhost:4000 (http)
Request: GET /auth/azure/callback?code=LONG_CODE&state=STATE&session_state=SESSION_STATE
** (exit) an exception was raised:
    ** (PowAssent.RequestError) invalid_resource
        (pow_assent) lib/pow_assent/phoenix/controllers/authorization_controller.ex:113: PowAssent.Phoenix.AuthorizationController.handle_strategy_error/1
        (pow_assent) lib/pow_assent/phoenix/controllers/authorization_controller.ex:1: PowAssent.Phoenix.AuthorizationController.action/2
        (pow_assent) lib/pow_assent/phoenix/controllers/authorization_controller.ex:1: PowAssent.Phoenix.AuthorizationController.phoenix_controller_pipeline/2
        (my_app) lib/my_app_web/endpoint.ex:1: MyAppWeb.Endpoint.instrument/4
        (phoenix) lib/phoenix/router.ex:278: Phoenix.Router.__call__/1
        (my_app) lib/my_app_web/endpoint.ex:1: MyAppWeb.Endpoint.plug_builder_call/2
        (my_app) lib/plug/debugger.ex:122: MyAppWeb.Endpoint."call (overridable 3)"/2
        (my_app) lib/my_app_web/endpoint.ex:1: MyAppWeb.Endpoint.call/2
        (plug) lib/plug/adapters/cowboy/handler.ex:16: Plug.Adapters.Cowboy.Handler.upgrade/4
        (cowboy) c:/Code/my_app/deps/cowboy/src/cowboy_protocol.erl:442: :cowboy_protocol.execute/4
[info] GET /auth/azure/callback
[debug] Processing with PowAssent.Phoenix.AuthorizationController.callback/2
  Parameters: %{"code" => "OTHER_LONG_CODE", "provider" => "azure", "session_state" => "SESSION_STATE", "state" => "STATE"}
  Pipelines: [:browser]
[info] Sent 500 in 62ms
[error] #PID<0.429.0> running MyAppWeb.Endpoint (cowboy_protocol) terminated
Server: localhost:4000 (http)
Request: GET /auth/azure/callback?code=OTHER_LONG_CODE&state=STATE&session_state=SESSION_STATE
** (exit) an exception was raised:
    ** (PowAssent.CallbackCSRFError) CSRF detected
        (pow_assent) lib/pow_assent/phoenix/controllers/authorization_controller.ex:113: PowAssent.Phoenix.AuthorizationController.handle_strategy_error/1
        (pow_assent) lib/pow_assent/phoenix/controllers/authorization_controller.ex:1: PowAssent.Phoenix.AuthorizationController.action/2
        (pow_assent) lib/pow_assent/phoenix/controllers/authorization_controller.ex:1: PowAssent.Phoenix.AuthorizationController.phoenix_controller_pipeline/2
        (my_app) lib/my_app_web/endpoint.ex:1: MyAppWeb.Endpoint.instrument/4
        (phoenix) lib/phoenix/router.ex:278: Phoenix.Router.__call__/1
        (my_app) lib/my_app_web/endpoint.ex:1: MyAppWeb.Endpoint.plug_builder_call/2
        (my_app) lib/plug/debugger.ex:122: MyAppWeb.Endpoint."call (overridable 3)"/2
        (my_app) lib/my_app_web/endpoint.ex:1: MyAppWeb.Endpoint.call/2
        (plug) lib/plug/adapters/cowboy/handler.ex:16: Plug.Adapters.Cowboy.Handler.upgrade/4
        (cowboy) c:/Code/my_app/deps/cowboy/src/cowboy_protocol.erl:442: :cowboy_protocol.execute/4

Note that I have removed the insanely long string codes/tokens from the call stack and replaced them with all caps strings instead (one per unique code/token). Just to make it easier to read.

Comment from our resident Azure AD guy when seeing the call stack:

What you can see now is that you have received a "code" back to your solution. This code has "lifetime" of 5 minuets, now you have to use this code and ask for the id_token for Azure AD. But this should be handled by your provider.

danschultzer commented 5 years ago

The CSFR happens because state=STATE differs from what is saved in your session. You should first just restart the app, and try again. It may just have been that you were testing with an old state.

leifericf commented 5 years ago

Hmmm. I've tried restarting the app a couple of times, but the issue persists. I suspect that maybe it has something to do with our Azure AD setup, parts of which might be on-premise. Maybe I'll need to use some different site than https://login.microsoftonline.com in my strategy config.

danschultzer commented 5 years ago

With this error you can be pretty sure that things are working on the Azure side of things. It's PowAssent that can't validate the request with the state. Do you have a standard Phoenix setup? The state is saved in the session. The state is set in the query when the user is redirected to the Azure login page, and that state is returned in the redirect uri query after sign in.

danschultzer commented 5 years ago

Hmm, I've tested locally with Azure, and works as expected. Not sure why it doesn't validate the state for you.

Is the state param same in the URI when you are redirect to azure, as when azure redirects you to the app? If it is, then the issue is with the session (either the value is being overridden, or the session is not persisted between requests for some reason).

leifericf commented 5 years ago

I went back and checked the error message from yesterday. I ca see that the session_state and session parameters were identical in the request and response, but the code parameter differed.

I did some more testing this morning, and it seems like the CSFR only occurs on subsequent attempts, after waiting for a couple of minutes. Below is a more detailed log with several attempts in two browsers tabs (without restarting the app) where the parameters are not removed (but abbreviated):

C:\Code\my_app>mix phx.server
[warn] Phoenix is unable to create symlinks. Phoenix' code reloader will run considerably faster if symlinks are allowed. On Windows, the lack of symlinks may even cause empty assets to be served. Luckily, you can address this issue by starting your Windows terminal at least once with "Run as Administrator" and then running your Phoenix application.
[info] Running MyAppWeb.Endpoint with Cowboy using http://0.0.0.0:4000
09:37:08 - info: compiled 6 files into 2 files, copied 3 in 1.2 sec

--------------------------------
First attempt in browser tab #1:
--------------------------------

[info] GET /auth/azure/new/
[debug] Processing with PowAssent.Phoenix.AuthorizationController.new/2
  Parameters: %{"provider" => "azure"}
  Pipelines: [:browser]
[info] Sent 302 in 31ms
[info] GET /auth/azure/callback
[debug] Processing with PowAssent.Phoenix.AuthorizationController.callback/2
  Parameters: %{"code" => "AQABAA...ja0gAA", "provider" => "azure", "session_state" => "dac33c...2a785f", "state" => "b5a2b2...fe13cb"}
  Pipelines: [:browser]
[info] Sent 500 in 516ms
[error] #PID<0.429.0> running MyAppWeb.Endpoint (cowboy_protocol) terminated
Server: localhost:4000 (http)
Request: GET /auth/azure/callback?code=AQABAA...ja0gAA&state=b5a2b2...fe13cb&session_state=dac33c...2a785f
** (exit) an exception was raised:
    ** (PowAssent.RequestError) invalid_resource
        (pow_assent) lib/pow_assent/phoenix/controllers/authorization_controller.ex:113: PowAssent.Phoenix.AuthorizationController.handle_strategy_error/1
        (pow_assent) lib/pow_assent/phoenix/controllers/authorization_controller.ex:1: PowAssent.Phoenix.AuthorizationController.action/2
        (pow_assent) lib/pow_assent/phoenix/controllers/authorization_controller.ex:1: PowAssent.Phoenix.AuthorizationController.phoenix_controller_pipeline/2
        (my_app) lib/my_app_web/endpoint.ex:1: MyAppWeb.Endpoint.instrument/4
        (phoenix) lib/phoenix/router.ex:278: Phoenix.Router.__call__/1
        (my_app) lib/my_app_web/endpoint.ex:1: MyAppWeb.Endpoint.plug_builder_call/2
        (my_app) lib/plug/debugger.ex:122: MyAppWeb.Endpoint."call (overridable 3)"/2
        (my_app) lib/my_app_web/endpoint.ex:1: MyAppWeb.Endpoint.call/2
        (plug) lib/plug/adapters/cowboy/handler.ex:16: Plug.Adapters.Cowboy.Handler.upgrade/4
        (cowboy) c:/Code/my_app/deps/cowboy/src/cowboy_protocol.erl:442: :cowboy_protocol.execute/4

-----------------------------------------------------------
Closed browser tab #1. First attempt in new browser tab #2:
-----------------------------------------------------------

[info] GET /auth/azure/new/
[debug] Processing with PowAssent.Phoenix.AuthorizationController.new/2
  Parameters: %{"provider" => "azure"}
  Pipelines: [:browser]
[info] Sent 302 in 0┬╡s
[info] GET /auth/azure/callback
[debug] Processing with PowAssent.Phoenix.AuthorizationController.callback/2
  Parameters: %{"code" => "AQABAA...NXQgAA", "provider" => "azure", "session_state" => "dac33c...2a785f", "state" => "52202c...dad628"}
  Pipelines: [:browser]
[info] Sent 500 in 266ms
[error] #PID<0.439.0> running MyAppWeb.Endpoint (cowboy_protocol) terminated
Server: localhost:4000 (http)
Request: GET /auth/azure/callback?code=AQABAA...NXQgAA&state=52202c...dad628&session_state=dac33c...2a785f
** (exit) an exception was raised:
    ** (PowAssent.RequestError) invalid_resource
        (pow_assent) lib/pow_assent/phoenix/controllers/authorization_controller.ex:113: PowAssent.Phoenix.AuthorizationController.handle_strategy_error/1
        (pow_assent) lib/pow_assent/phoenix/controllers/authorization_controller.ex:1: PowAssent.Phoenix.AuthorizationController.action/2
        (pow_assent) lib/pow_assent/phoenix/controllers/authorization_controller.ex:1: PowAssent.Phoenix.AuthorizationController.phoenix_controller_pipeline/2
        (my_app) lib/my_app_web/endpoint.ex:1: MyAppWeb.Endpoint.instrument/4
        (phoenix) lib/phoenix/router.ex:278: Phoenix.Router.__call__/1
        (my_app) lib/my_app_web/endpoint.ex:1: MyAppWeb.Endpoint.plug_builder_call/2
        (my_app) lib/plug/debugger.ex:122: MyAppWeb.Endpoint."call (overridable 3)"/2
        (my_app) lib/my_app_web/endpoint.ex:1: MyAppWeb.Endpoint.call/2
        (plug) lib/plug/adapters/cowboy/handler.ex:16: Plug.Adapters.Cowboy.Handler.upgrade/4
        (cowboy) c:/Code/my_app/deps/cowboy/src/cowboy_protocol.erl:442: :cowboy_protocol.execute/4

---------------------------------
Second attempt in browser tab #2:
---------------------------------

[info] GET /auth/azure/new/
[debug] Processing with PowAssent.Phoenix.AuthorizationController.new/2
  Parameters: %{"provider" => "azure"}
  Pipelines: [:browser]
[info] Sent 302 in 0┬╡s
[info] GET /auth/azure/callback
[debug] Processing with PowAssent.Phoenix.AuthorizationController.callback/2
  Parameters: %{"code" => "AQABAA...mKAgAA", "provider" => "azure", "session_state" => "dac33c...2a785f", "state" => "a6efd6...621ae3"}
  Pipelines: [:browser]
[info] Sent 500 in 250ms
[error] #PID<0.447.0> running MyAppWeb.Endpoint (cowboy_protocol) terminated
Server: localhost:4000 (http)
Request: GET /auth/azure/callback?code=AQABAA...mKAgAA&state=a6efd6...621ae3&session_state=dac33c...2a785f
** (exit) an exception was raised:
    ** (PowAssent.RequestError) invalid_resource
        (pow_assent) lib/pow_assent/phoenix/controllers/authorization_controller.ex:113: PowAssent.Phoenix.AuthorizationController.handle_strategy_error/1
        (pow_assent) lib/pow_assent/phoenix/controllers/authorization_controller.ex:1: PowAssent.Phoenix.AuthorizationController.action/2
        (pow_assent) lib/pow_assent/phoenix/controllers/authorization_controller.ex:1: PowAssent.Phoenix.AuthorizationController.phoenix_controller_pipeline/2
        (my_app) lib/my_app_web/endpoint.ex:1: MyAppWeb.Endpoint.instrument/4
        (phoenix) lib/phoenix/router.ex:278: Phoenix.Router.__call__/1
        (my_app) lib/my_app_web/endpoint.ex:1: MyAppWeb.Endpoint.plug_builder_call/2
        (my_app) lib/plug/debugger.ex:122: MyAppWeb.Endpoint."call (overridable 3)"/2
        (my_app) lib/my_app_web/endpoint.ex:1: MyAppWeb.Endpoint.call/2
        (plug) lib/plug/adapters/cowboy/handler.ex:16: Plug.Adapters.Cowboy.Handler.upgrade/4
        (cowboy) c:/Code/my_app/deps/cowboy/src/cowboy_protocol.erl:442: :cowboy_protocol.execute/4

---------------------------------------------------------------------------
Third attempt in browser tab #2, after waiting a few minutes (CSRF occurs):
---------------------------------------------------------------------------

[info] GET /auth/azure/new/
[debug] Processing with PowAssent.Phoenix.AuthorizationController.new/2
  Parameters: %{"provider" => "azure"}
  Pipelines: [:browser]
[info] Sent 302 in 0┬╡s
[info] GET /auth/azure/callback
[debug] Processing with PowAssent.Phoenix.AuthorizationController.callback/2
  Parameters: %{"code" => "AQABAA...APkgAA", "provider" => "azure", "session_state" => "dac33c...2a785f", "state" => "6244a2...f2e923"}
  Pipelines: [:browser]
[info] Sent 500 in 250ms
[error] #PID<0.456.0> running MyAppWeb.Endpoint (cowboy_protocol) terminated
Server: localhost:4000 (http)
Request: GET /auth/azure/callback?code=AQABAA...APkgAA&state=6244a2...f2e923&session_state=dac33c...2a785f
** (exit) an exception was raised:
    ** (PowAssent.RequestError) invalid_resource
        (pow_assent) lib/pow_assent/phoenix/controllers/authorization_controller.ex:113: PowAssent.Phoenix.AuthorizationController.handle_strategy_error/1
        (pow_assent) lib/pow_assent/phoenix/controllers/authorization_controller.ex:1: PowAssent.Phoenix.AuthorizationController.action/2
        (pow_assent) lib/pow_assent/phoenix/controllers/authorization_controller.ex:1: PowAssent.Phoenix.AuthorizationController.phoenix_controller_pipeline/2
        (my_app) lib/my_app_web/endpoint.ex:1: MyAppWeb.Endpoint.instrument/4
        (phoenix) lib/phoenix/router.ex:278: Phoenix.Router.__call__/1
        (my_app) lib/my_app_web/endpoint.ex:1: MyAppWeb.Endpoint.plug_builder_call/2
        (my_app) lib/plug/debugger.ex:122: MyAppWeb.Endpoint."call (overridable 3)"/2
        (my_app) lib/my_app_web/endpoint.ex:1: MyAppWeb.Endpoint.call/2
        (plug) lib/plug/adapters/cowboy/handler.ex:16: Plug.Adapters.Cowboy.Handler.upgrade/4
        (cowboy) c:/Code/my_app/deps/cowboy/src/cowboy_protocol.erl:442: :cowboy_protocol.execute/4
[info] GET /auth/azure/callback
[debug] Processing with PowAssent.Phoenix.AuthorizationController.callback/2
  Parameters: %{"code" => "AQABAA...UmUgAA", "provider" => "azure", "session_state" => "dac33c...2a785f", "state" => "6244a2...f2e923"}
  Pipelines: [:browser]
[info] Sent 500 in 0┬╡s
[error] #PID<0.457.0> running MyAppWeb.Endpoint (cowboy_protocol) terminated
Server: localhost:4000 (http)
Request: GET /auth/azure/callback?code=AQABAA...UmUgAA&state=6244a2...f2e923&session_state=dac33c...2a785f
** (exit) an exception was raised:
    ** (PowAssent.CallbackCSRFError) CSRF detected
        (pow_assent) lib/pow_assent/phoenix/controllers/authorization_controller.ex:113: PowAssent.Phoenix.AuthorizationController.handle_strategy_error/1
        (pow_assent) lib/pow_assent/phoenix/controllers/authorization_controller.ex:1: PowAssent.Phoenix.AuthorizationController.action/2
        (pow_assent) lib/pow_assent/phoenix/controllers/authorization_controller.ex:1: PowAssent.Phoenix.AuthorizationController.phoenix_controller_pipeline/2
        (my_app) lib/my_app_web/endpoint.ex:1: MyAppWeb.Endpoint.instrument/4
        (phoenix) lib/phoenix/router.ex:278: Phoenix.Router.__call__/1
        (my_app) lib/my_app_web/endpoint.ex:1: MyAppWeb.Endpoint.plug_builder_call/2
        (my_app) lib/plug/debugger.ex:122: MyAppWeb.Endpoint."call (overridable 3)"/2
        (my_app) lib/my_app_web/endpoint.ex:1: MyAppWeb.Endpoint.call/2
        (plug) lib/plug/adapters/cowboy/handler.ex:16: Plug.Adapters.Cowboy.Handler.upgrade/4
        (cowboy) c:/Code/my_app/deps/cowboy/src/cowboy_protocol.erl:442: :cowboy_protocol.execute/4

---------------------------------
Fourth attempt in browser tab #2:
---------------------------------

[info] GET /auth/azure/new/
[debug] Processing with PowAssent.Phoenix.AuthorizationController.new/2
  Parameters: %{"provider" => "azure"}
  Pipelines: [:browser]
[info] Sent 302 in 0┬╡s
[info] GET /auth/azure/callback
[debug] Processing with PowAssent.Phoenix.AuthorizationController.callback/2
  Parameters: %{"code" => "AQABAA...EckgAA", "provider" => "azure", "session_state" => "dac33c...2a785f", "state" => "3f2ada...91a963"}
  Pipelines: [:browser]
[info] Sent 500 in 282ms
[error] #PID<0.466.0> running MyAppWeb.Endpoint (cowboy_protocol) terminated
Server: localhost:4000 (http)
Request: GET /auth/azure/callback?code=AQABAA...EckgAA&state=3f2ada...91a963&session_state=dac33c...2a785f
** (exit) an exception was raised:
    ** (PowAssent.RequestError) invalid_resource
        (pow_assent) lib/pow_assent/phoenix/controllers/authorization_controller.ex:113: PowAssent.Phoenix.AuthorizationController.handle_strategy_error/1
        (pow_assent) lib/pow_assent/phoenix/controllers/authorization_controller.ex:1: PowAssent.Phoenix.AuthorizationController.action/2
        (pow_assent) lib/pow_assent/phoenix/controllers/authorization_controller.ex:1: PowAssent.Phoenix.AuthorizationController.phoenix_controller_pipeline/2
        (my_app) lib/my_app_web/endpoint.ex:1: MyAppWeb.Endpoint.instrument/4
        (phoenix) lib/phoenix/router.ex:278: Phoenix.Router.__call__/1
        (my_app) lib/my_app_web/endpoint.ex:1: MyAppWeb.Endpoint.plug_builder_call/2
        (my_app) lib/plug/debugger.ex:122: MyAppWeb.Endpoint."call (overridable 3)"/2
        (my_app) lib/my_app_web/endpoint.ex:1: MyAppWeb.Endpoint.call/2
        (plug) lib/plug/adapters/cowboy/handler.ex:16: Plug.Adapters.Cowboy.Handler.upgrade/4
        (cowboy) c:/Code/my_app/deps/cowboy/src/cowboy_protocol.erl:442: :cowboy_protocol.execute/4
danschultzer commented 5 years ago

Ah, seems like you get invalid_resource error.

I've set up the following in my configuration:

azure: [
  client_id: "CLIENT_ID"
  client_secret: "CLIENT_SECRET",
  tenant_id: "TENANT_ID",
  authorization_params: [response_mode: "query", response_type: "code", resource: "https://graph.microsoft.com"],
  strategy: PowAssent.Strategy.AzureOAuth2
]

I don't know enough about how azure works to understand the tenant/resource configuration. I just used https://graph.microsoft.com/ for resource.

Here's the description for the resource param:

The App ID URI of the target web API (secured resource). To find the App ID URI, in the Azure Portal, click Azure Active Directory, click Application registrations, open the application's Settings page, then click Properties. It may also be an external resource like https://graph.microsoft.com. This is required in one of either the authorization or token requests. To ensure fewer authentication prompts place it in the authorization request to ensure consent is received from the user.

And the tenant id:

The {tenant} value in the path of the request can be used to control who can sign into the application. The allowed values are tenant identifiers, for example, 8eaef023-2b34-4da1-9baa-8bc8c9d6a490 or contoso.onmicrosoft.com or common for tenant-independent tokens

I've an idea that it works this way because the resource (user) can be pulled from data you manage on azure or from your own platform (the OAuth2 guide shows an example with http://service.contoso.com/), rather than from the actual Microsoft account users.

danschultzer commented 5 years ago

Hmm, from what I read it seems like the resource param is mandatory. Maybe it'll be easiest for developers, if I add a default for resource with https://graph.microsoft.com. I assume this will pull their MS account details, and this is what most would need.

belaie commented 5 years ago

resource parameter is part of v1 endpoint, its not purre Oauth 2.0 specs, but this shoudl be defined as parameter so one can define for which resource the client would acuquire tokens for

danschultzer commented 5 years ago

Thanks for explaining that @belaie. I've added a resource parameter that can be overridden but defaults to https://graph.microsoft.com. The configuration can look like this (based on the values given in the OAuth2 guide from the Azure website):

azure: [
  client_id: "REPLACE_WITH_CLIENT_ID",
  client_secret: "REPLACE_WITH_CLIENT_SECRET",
  tenant_id: "8eaef023-2b34-4da1-9baa-8bc8c9d6a490",
  resource: "https://service.contoso.com/",
  strategy: PowAssent.Strategy.AzureOAuth2
]

Hopefully everything will work now @IRLeif!

leifericf commented 5 years ago

Very cool! Got it working now.

Note that I needed to pass the client_id as the resource, instead of the app ID URI. I'm not sure why exactly, but more details about this can be found in this answer on Stack Overflow.

When I used the app ID URI as the resource, I got this error:

[info] GET /auth/azure/new/
[debug] Processing with PowAssent.Phoenix.AuthorizationController.new/2
  Parameters: %{"provider" => "azure"}
  Pipelines: [:browser]
[info] Sent 302 in 0┬╡s
[info] GET /auth/azure/callback
[debug] Processing with PowAssent.Phoenix.AuthorizationController.callback/2
  Parameters: %{"error" => "invalid_request", "error_description" => "AADSTS90009: Application 'e8b...' is requesting a token for itself. This scenario is supported only if resource is specified using the GUID based App Identifier.\r\nTrace ID: 674...\r\nCorrelation ID: d43...\r\nTimestamp: 2018-09-14 08:23:55Z", "provider" => "azure", "state" => "33f..."}
  Pipelines: [:browser]
[info] Sent 500 in 16ms
[error] #PID<0.564.0> running MyAppWeb.Endpoint (cowboy_protocol) terminated
Server: localhost:4000 (http)
Request: GET /auth/azure/callback?error=invalid_request&error_description=AADSTS90009%3a+Application+%27e8b...%27+is+requesting+a+token+for+itself.+This+scenario+is+supported+only+if+resource+is+specified+using+the+GUID+based+App+Identifier.%0d%0aTrace+ID%3a+674...%0d%0aCorrelation+ID%3a+d43...%0d%0aTimestamp%3a+2018-09-14+08%3a23%3a55Z&state=33f...
** (exit) an exception was raised:
    ** (PowAssent.CallbackError) AADSTS90009: Application 'e8b...' is requesting a token for itself. This scenario is supported only if resource is specified using the GUID based App Identifier.
Trace ID: 674...
Correlation ID: d43...
Timestamp: 2018-09-14 08:23:55Z
        (pow_assent) lib/pow_assent/phoenix/controllers/authorization_controller.ex:113: PowAssent.Phoenix.AuthorizationController.handle_strategy_error/1
        (pow_assent) lib/pow_assent/phoenix/controllers/authorization_controller.ex:1: PowAssent.Phoenix.AuthorizationController.action/2
        (pow_assent) lib/pow_assent/phoenix/controllers/authorization_controller.ex:1: PowAssent.Phoenix.AuthorizationController.phoenix_controller_pipeline/2
        (my_app) lib/my_app_web/endpoint.ex:1: MyAppWeb.Endpoint.instrument/4
        (phoenix) lib/phoenix/router.ex:278: Phoenix.Router.__call__/1
        (my_app) lib/my_app_web/endpoint.ex:1: MyAppWeb.Endpoint.plug_builder_call/2
        (my_app) lib/plug/debugger.ex:122: MyAppWeb.Endpoint."call (overridable 3)"/2
        (my_app) lib/my_app_web/endpoint.ex:1: MyAppWeb.Endpoint.call/2
        (plug) lib/plug/adapters/cowboy/handler.ex:16: Plug.Adapters.Cowboy.Handler.upgrade/4
        (cowboy) c:/Code/my_app/deps/cowboy/src/cowboy_protocol.erl:442: :cowboy_protocol.execute/4
danschultzer commented 5 years ago

Cool, so it's just Azure configuration, and not anything missing in the integration! I've just copied the instructions from Azures own guide so I'll keep it at that. Will get it merged in and release a new version 🚀

danschultzer commented 5 years ago

v0.1.0-alpha.12 is released.

leifericf commented 5 years ago

Awesome, thank you again for all the help, @danschultzer. I have learned a lot through this process and I'm looking forward to being able to contribute back once I've got some more experience.

danschultzer commented 4 years ago

For future reference; PowAssent.Strategy.AzureOAuth2 has been changed to Assent.Strategy.AzureAD and now uses OIDC. It requires a nonce, but with PowAssent you just have to add nonce: true to the provider config.