Session params missing for oidc providers using response_mode: form_post (e.g. Azure AD)

I am trying to use the OIDC strategy with Azure AD. Now is Azure requiring to use response_mode: form_post. This requires that the POST callback endpoint is not under CSRF protection (similar to how pow_assent is doing it). The issue is that the user/<strategy> key is not in the session during the callback phase. Am I required to implement a server side session store as pow does it? See the setup below:
defmodule MyApp.Router do
...
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_live_flash
plug :put_root_layout, html: {MyAppWeb.Layouts, :root}
plug :protect_from_forgery
plug :put_secure_browser_headers
plug :load_from_session
end

pipeline :skip_csrf_protection do
plug(:accepts, ["html"])
plug(:fetch_session)
plug(:fetch_flash)
plug(:put_secure_browser_headers)
end

scope "/", MyAppWeb do
pipe_through(:skip_csrf_protection)

auth_routes_for(MyApp.Accounts.User, to: AuthController)
end

scope "/", MyAppWeb do
pipe_through :browser

sign_in_route()
sign_out_route(AuthController)

get "/", PageController, :home
end
...
end
defmodule MyApp.Router do
...
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_live_flash
plug :put_root_layout, html: {MyAppWeb.Layouts, :root}
plug :protect_from_forgery
plug :put_secure_browser_headers
plug :load_from_session
end

pipeline :skip_csrf_protection do
plug(:accepts, ["html"])
plug(:fetch_session)
plug(:fetch_flash)
plug(:put_secure_browser_headers)
end

scope "/", MyAppWeb do
pipe_through(:skip_csrf_protection)

auth_routes_for(MyApp.Accounts.User, to: AuthController)
end

scope "/", MyAppWeb do
pipe_through :browser

sign_in_route()
sign_out_route(AuthController)

get "/", PageController, :home
end
...
end
and the strategy
# MyApp.Accounts.User
...
strategies do
oidc :azure_ad do
client_id "***"
client_secret "***"
site "https://login.microsoftonline.com/***/v2.0"
redirect_uri("https://***/auth")

authorization_params(
scope: "profile email",
response_mode: "form_post"
)

authorize_url("/authorize")
token_url("/token")
client_authentication_method(:client_secret_post)
nonce(true)
identity_resource(MyApp.Accounts.UserIdentity)
end
end
...
# MyApp.Accounts.User
...
strategies do
oidc :azure_ad do
client_id "***"
client_secret "***"
site "https://login.microsoftonline.com/***/v2.0"
redirect_uri("https://***/auth")

authorization_params(
scope: "profile email",
response_mode: "form_post"
)

authorize_url("/authorize")
token_url("/token")
client_authentication_method(:client_secret_post)
nonce(true)
identity_resource(MyApp.Accounts.UserIdentity)
end
end
...
3 Replies
jart
jart2y ago
Hey there. I’m not sure I follow - OIDC has a lot of variety and I personally find it all pretty confusing. Some general advice until I understand the problem better; AshAuthentication uses assent under the covers. The DSL tries to generate an assent configuration and handle the ashy bits for you. You can dive into AshAuthentication.Strategy.OAuth2.Plug.config_for/1 (it’s private but maybe we should make it public/undocumented for debugging purposes) to see what the generated configuration looks like and whether it’s what you expect.
bziegler
bzieglerOP2y ago
Totally agree that it is a bit confusing and sorry for the poor explanation. Let me try to pin point the issue a bit better. AshAuthentication.Strategy.OAuth2.Plug.request puts the session_params under the session_key in the :plug_session (it's private), e.g.
%Plug.Conn{
...
private: %{
...
:plug_session => %{
"_csrf_token" => "...",
"user/azure_ad" => %{
state: "....",
nonce: "...."
},
...
}
}
}
%Plug.Conn{
...
private: %{
...
:plug_session => %{
"_csrf_token" => "...",
"user/azure_ad" => %{
state: "....",
nonce: "...."
},
...
}
}
}
And that is all fine. The issue is that :plug_session is empty when AshAuthentication.Strategy.OAuth2.Plug.callback gets called and therefore session_params cannot be loaded into the config anymore (used by assent to do the nonce and state checks). I created a little hack to save the :session_params in a file and load during the callback. With that in place, the assent strategy works but it's of course not the path to go. I don't think the config_for can help us here as the required session_params are not in the :strategy part of the conn. As I understand the pow implementation correct, they are storing the session_params in an ETS or Mnesia store which I mimic with my hacky file implementation. I also checked my setup with the Github strategy and all works there as expected (only GET requests in the OIDC/Oauth2 strategy). I hope this makes it a bit easier to understand. I am also happy to contribute with a PR but would like your opinion on the matter first. While on it, I thought of doing one PR for an AzureAD strategy to handle tenant_id via the DSL and to add a convenience function for creating the POST callback endpoints to be placed in a :skip_csrf_protection scope (you can check the pow_assent docs, where they have a similar helper function called pow_assent_authorization_post_callback_routes). So, I changed the session store to an ETS table now and all works as intended. The remaining question would be why in a cookie session setup the session_params isn't stored or fetched correctly. Any idea?
Carl
Carl15mo ago
Hey @bziegler , I'd be very interested to hear how this turned for you in the end. I'm going to engage a consultant for this matter and the consultant and I would appreciate any input. Also, we've talked about contributing any insights back to the community (PR, blog post).

Did you find this page helpful?