Skip to content

Conversation

@joelparkerhenderson
Copy link
Contributor

No description provided.

Microsoft Azure currently creates new applications with scope "User.Read" which includes sign in and basic profile access.

This patch fixes the documentation by adding the scope for User.Read, and also by adding quotation marks so the export works correctly with spaces.
…ist-entry-for-user-read

Add scope for User.Read to fix security permissions
@LuchoTurtle
Copy link
Member

Hey Joel! Thanks for the contribution!
Can you add a bit more context why you want to add User.Read as a default? 😄

@joelparkerhenderson
Copy link
Contributor Author

Yes thank you. As context, I'm I'm teaching a large group of technical staff about Entra & AAD auth for my organization, and I'm seeking a step-by-step setup to add Entra/AAD to Phoenix for technical staff who are relative newcomers to Phoenix.

Your work is excellent and I appreciate you sharing it so much. When I tried it yesterday, I found a couple of surprises that were blockers, presumably due to me not quite understanding Entra/AAD. The hardest one happened when I got the sign in button running, it successfully hit AAD, and returned a token-- but then the profile lookup failed with "UnknownError".

What I discovered when I looked at the AAD configuration of the brand new app, AAD gave it the scope of "User.Read" and did not give it an explicit scope of "profile" or "openid". When I added the scope of "User.Read" to the env var, and also put quotations around the env var value, then the app succeeded at fetching the profile.

If you're curious, see the demo app at: https://github.com/joelparkerhenderson/demo-elixir-auth-microsoft

In the demo app that I am doing, I'm aiming to be explicit, such as with env vars, function names, template names, and having all the pieces use the name prefix "AuthMicrosoft", more doc comments, etc. This is to help learners. It's akin to configuration-over-convention for the purposes of learning the first time.

As an aside, beyond this PR, one quick item of your demo that I don't understand yet is the state for CORS, such as where your demo says "random_state_uid".

Thank you so much for the work. It's a big help.

@LuchoTurtle
Copy link
Member

LuchoTurtle commented Sep 2, 2025

Hey Joel, thanks for the feedback, knowing you're teaching people authentication with this package makes me chuffed to bits!

You're right about the User.Read scope issue. I wasn't clear on the README and I didn't properly differentiate between Azure AD/Entra ID and normal OpenID Connect scopes.

When you create an Entra ID app registration, Microsoft automatically grants it the User.Read scope by default, but this doesn't automatically translate to the OpenID Connect scopes (openid, profile) that our library requests by default.

The User.Read scope is a Microsoft Graph API permission, while openid and profile are OpenID Connect scopes. They serve different purposes:

  • openid profile - for basic authentication and getting basic profile info via the ID token
  • User.Read - for calling Microsoft Graph API to get detailed user information

I'll merge this PR so it requests this scope by default on the demo for those that are using Azure.


Regarding the state parameter (the random_state_uid you mentioned), it isn't so much related to CORS but it's actually a CSRF (Cross-Site Request Forgery) protection mechanism. Here's how it works:

  1. When starting OAuth flow* the app generates a random, unpredictable value and includes it in the authorization URL.
  2. Microsoft stores it: Microsoft's OAuth server remembers this value during the auth process.
  3. On callback, Microsoft sends the same state value back to the app.
  4. The app verifies that the returned state matches what you originally sent.

The "random_state_uid" in our demo is indeed a simplified example. In production, it should be a cryptographically secure random value that is stored in session and verified on callback to prevent CSRF attacks.

  • Stored in the session
  • Verified on callback to prevent CSRF attacks

To compare, this is how the demo is right now:

def index(conn, %{"code" => code, "state" => state}) do
  if state !== "random_state_uid" do
    # ...
  end
  # ....
end

But a more secure implementation, for production environments, would be something like:

# When generating the OAuth URL:
def login(conn, _params) do
  state = :crypto.strong_rand_bytes(32) |> Base.url_encode64()
  conn = put_session(conn, :oauth_state, state)
  
  oauth_url = ElixirAuthMicrosoft.generate_oauth_url_authorize(conn, state)
  redirect(conn, external: oauth_url)
end

# In the callback:
def index(conn, %{"code" => code, "state" => returned_state}) do
  stored_state = get_session(conn, :oauth_state)
  
  if returned_state != stored_state do
    conn
    |> put_flash(:error, "Invalid state parameter")
    |> redirect(to: "/")
  else
    conn = delete_session(conn, :oauth_state) 
    {:ok, token} = ElixirAuthMicrosoft.get_token(code, conn)
    # ... rest of auth logic
  end
end

This prevents CRSF attacks 😄 .

Anyway, I'll merge this PR. Thanks for the contribution! Let me know if you need any help :D

@LuchoTurtle LuchoTurtle self-requested a review September 2, 2025 17:11
@LuchoTurtle LuchoTurtle merged commit 4a10cd7 into dwyl:main Sep 2, 2025
1 check passed
Copy link
Member

@nelsonic nelsonic left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@joelparkerhenderson great addition. Thanks. 🙏
Always good to get practical enhancements from people using the package in the wild! 👌
@LuchoTurtle will you publish a new version to ship this change or should I? 📦 :shipit:

@LuchoTurtle
Copy link
Member

No need @nelsonic , it's just a README change 👍

@nelsonic
Copy link
Member

nelsonic commented Sep 2, 2025

@LuchoTurtle my bad for not spotting that it was a README.md update. Thought it was a default config update. 💭🙃👌

hope you gents are having a great week so far. 🤞🏼

@joelparkerhenderson
Copy link
Contributor Author

Much obliged, thank you. Your CSRF code is super-helpful. For what it's worth, I'd advocate for adding that CSRF code to the README as well, because it's not obvious (IMHO) and it's an important security step. Thanks! <3

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants