AzureAD / microsoft-authentication-library-for-java

Microsoft Authentication Library (MSAL) for Java http://aka.ms/aadv2
MIT License
288 stars 144 forks source link

[Bug] Cannot call the OIDC endpoint with access token from the broker #835

Open marbon87 opened 4 months ago

marbon87 commented 4 months ago

Library version used

1.16.0

Java version

21

Is this a new or an existing app?

This is a new app or experiment

Summary of the issue

  1. An external component (KeyCloak) integrates with OIDC providers by making a call to the userinfo_endpoint, which it reads from the OIDC endpoint.
  2. I can get a WAM token for Graph, but I cannot call the endpoint (401). I am able to call when using a browser!

OIDC endpoint: https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration UserInfo endpoint: https://graph.microsoft.com/oidc/userinfo Scope requested: User.Read

See a simple repo (in C#) in https://github.com/AzureAD/microsoft-authentication-library-for-java/issues/835#issuecomment-2310442115

Note that I can call https://graph.microsoft.com/v1.0/me endpoint with the WAM token.

Issue description and reproduction steps

We are using keycloak as an internal idp and want to use the external to internal token exchange feature.

Therefor i acquire a token silently with MSAL4j and post the access token to keycloak. The problem is that keycloak call the MS Graph userinfo-Endpoint but get's the error: "Token must contain sub claim."

When i acquire an access token by calling the following uri in the browser and use the access-token from the redirect, the token exchange is working:

https://login.microsoftonline.com/my-tenant/oauth2/v2.0/authorize?client_id=my-client-id&response_type=token+id_token&redirect_uri=https://localhost&scope=user.read+openid+profile+email&response_mode=fragment&state=12345&nonce=678910

I compared the two access tokens from MSAL4j and in the browser and guess that the problem in the MSAL4j-access token is the missing xms_st.sub-Claim in the access token.

What do i have to configure, to get that scope in the access token from MSAL4j?

Relevant code snippets

package org.example;

import com.microsoft.aad.msal4j.IAuthenticationResult;
import com.microsoft.aad.msal4j.MsalException;
import com.microsoft.aad.msal4j.PublicClientApplication;
import com.microsoft.aad.msal4j.SilentParameters;
import com.microsoft.aad.msal4jbrokers.Broker;

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.Set;

public class Example {

    private static final Set<String> scope = Set.of("user.read", "openid", "profile", "upn", "preferred_username");
    private static final String clientId = "my-client-id";

    public static void main(String args[]) throws Exception {
        Broker broker = new Broker.Builder()
                .supportWindows(true).build();

        PublicClientApplication pca = PublicClientApplication.builder(clientId)
                .broker(broker)
                .build();

        IAuthenticationResult result = acquireTokenIntegratedWindowsAuth(pca, scope);
        System.out.println("Account username:  " + result.account().username());
        System.out.println("Access token:      " + result.accessToken());
        System.out.println("Id token:          " + result.idToken());

        HttpClient client = HttpClient.newHttpClient();

        HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create("https://graph.microsoft.com/oidc/userinfo"))
                .header("Authorization", "Bearer " + result.accessToken())
                .build();

        HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());

        System.out.println("User-Info Status:              " + response.statusCode());
        System.out.println("User-Info Result:              " + response.body());

    }

    private static IAuthenticationResult acquireTokenIntegratedWindowsAuth(PublicClientApplication pca,
                                                                           Set<String> scopes) throws Exception {

        IAuthenticationResult result;
        try {
            SilentParameters silentParameters =
                    SilentParameters
                            .builder(scopes)
                            .tenant("<my-tenent>")
                            .forceRefresh(true)
                            .scopes(Set.of("https://graph.microsoft.com/.default"))
                            .build();
            result = pca.acquireTokenSilently(silentParameters).join();
        } catch (MsalException ex) {
            throw ex;
        }
        return result;
    }
}

Expected behavior

MS Graph API Userinfo-Endpoint should respond with status code 200.

Identity provider

Microsoft Entra ID (Work and School accounts and Personal Microsoft accounts)

Regression

No response

Solution and workarounds

No response

Avery-Dunn commented 4 months ago

Hello @marbon87 : Just to clarify a couple of things:

After some testing I'm having trouble reproducing your exact issue, my tokens either correctly have that "xms_st" claim or I run into a different issue before getting the token.

However, my initial thought is that by default we will set the authority to https://login.microsoftonline.com/common/, and I noticed that your working URL has https://login.microsoftonline.com/my-tenant/. So, you may need to specify an authority in your PublicClientApplication such as https://login.microsoftonline.com/your-tenant-id/ in order for the access token to be created with the right ID token info.

marbon87 commented 4 months ago
  • When you say that you can get working tokens in the browser you mean you're getting them from that URL manually, right? Not from MSAL's interactive/browser-based flow?

Correct.

  • For the working tokens, I assume the ID token has a "sub" claim that matches what is in the "xms_st" claim of the access token.

Correct.

  • Is the "sub" in the working ID token the same as the "sub" in ID token you get from MSAL? (and the access token from MSAL is just missing "xms_st")

Correct: Both id tokens have the same sub-claim, starting with -qr.... This value is equal to the xms_st.sub-Claim in the working acsess-token. The sub-claim of the workin access-token matches the sub-claim from MSAL. The MSAL-access-token totally misses the xms_st.sub-Claim.

After some testing I'm having trouble reproducing your exact issue, my tokens either correctly have that "xms_st" claim or I run into a different issue before getting the token.

However, my initial thought is that by default we will set the authority to https://login.microsoftonline.com/common/, and I noticed that your working URL has https://login.microsoftonline.com/my-tenant/. So, you may need to specify an authority in your PublicClientApplication such as https://login.microsoftonline.com/your-tenant-id/ in order for the access token to be created with the right ID token info.

I set the authority but get the same error:

        PublicClientApplication pca = PublicClientApplication.builder(clientId)
                .broker(broker)
                .authority("https://login.microsoftonline.com/tenant-id")
                .build();
bgavrilMS commented 4 months ago

Can you clarify if you are using the Id Token or the access token?

The broker uses Entra's v1 endpoint, and so Id Tokens will be v1, which are somewhat different than the v2 endpoint Id Tokens (which you are using). Access tokens however should be the same, they are determined by the resource (Graph).

marbon87 commented 4 months ago

Hi @bgavrilMS , i am using only access tokens to call the userinfo endpoint as shown in the example above.

marbon87 commented 4 months ago

Is https://graph.microsoft.com/.default the correct URI for the userinfo endpoint?

If i try https://login.microsoftonline.com/tenant-idopenid/userinfo i also get a 400 reponse with WWW-Authenticate-Header:

Bearer correlation_id="..........", error="invalid_request", error_codes="[9001014]", error_description="AADSTS9001014: This token was not issued for the UserInfo endpoint. This may have been a token for Graph or another resource. Trace ID: f7b3c682-7356-4cc7-a42f-3fc9d7bd5e00 Correlation ID: 49f31763-c0ff-4dab-8e88-32dd1725dcfb Timestamp: 2024-07-09 08:16:49Z", error_uri="https://login.microsoftonline.com/error?code=9001014", timestamp="2024-07-09 08:16:49Z", trace_id="f7b3c682-7356-4cc7-a42f-3fc9d7bd5e00"
marbon87 commented 3 months ago

Hi @bgavrilMS , do you have any updates on this?

bgavrilMS commented 3 months ago

The userinfo endpoint is part of the OIDC document, which all IdP are required to publish. You can find Entra's here:

https://login.microsoftonline.com/common/.well-known/openid-configuration

I can imagine that KeyCloak will try to find the userinfo endpoint by looking at the iss claim from the AAD token and adding /.well-known to it.

Let me try this out on a personal tenant...

bgavrilMS commented 3 months ago

@marbon87 - let me see if I understand correctly: who calls the userinfo endpoint ? KeyCloak or your app?

I wasn't able to get a token for the user endpoint, Entra keeps giving me a token with Graph scopes. And the user endpoint refuses it - with error error_description="AADSTS9001014: This token was not issued for the UserInfo endpoint. This may have been a token for Graph or another resource."

Is this what you are getting? (the error message is part of the 400)

marbon87 commented 3 months ago

@marbon87 - let me see if I understand correctly: who calls the userinfo endpoint ? KeyCloak or your app?

Keycloak calls the userinfo enpdoint.

I wasn't able to get a token for the user endpoint, Entra keeps giving me a token with Graph scopes. And the user endpoint refuses it - with error error_description="AADSTS9001014: This token was not issued for the UserInfo endpoint. This may have been a token for Graph or another resource."

Is this what you are getting? (the error message is part of the 400)

Exactly.

bgavrilMS commented 3 months ago

Ah ok, I think I figured it out. Entra has v1 and v2 endpoints. The OIDC document for v2 is here (notice the v2.0 segment)

https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration

And so the userinfo endpoint is: image

The userinfo is indeed hosted by Graph, but this is an implementation detail.

The key point here is that when you configure federation between KeyCloak and Entra, you must tell KeyCloak to use the v2 endpoints of AAD. So the authority from KeyCloak's perspective is https://login.microsoftonline.com/common/v2.0/. MSAL libraries "abstract" this away and will add "v2.0" on their own.

HTH

marbon87 commented 3 months ago

Sorry, but this is also not working. If i call https://graph.microsoft.com/oidc/userinfo manually (with curl) with the access-token from MSAL4j i get the following error:

{"error":{"code":"UnknownError","message":"Token must contain sub claim.","innerError":{"date":"2024-08-26T12:32:37","request-id":"2047b161-fcf0-4fe1-ad7d-55eb8deafac6"
bgavrilMS commented 3 months ago

How does your token look like? (make sure to hide any PII like name etc) Here's mine from https://jwt.ms

image

marbon87 commented 3 months ago

I sent it to you by email. Could you share your java code to get the token? Mine is above.

bgavrilMS commented 3 months ago

Thanks I got it. I think it's a broker issue.

I am able to get a response if I use the browser to authenticate, but I get the Token must contain sub claim error if I use the broker. Obvs the token contains the sub claim (it's mandatory per OIDC).

Here's my C# code that repros the issue.

internal class Program
{
    private const string ClientId = "3bee2617-ab99-4ba5-b390-be397057344f";
    //private const string TenantId = "839846c0-9cef-4455-9542-0c36d831d026";
    private const string TenantId = "organizations";
    private static readonly Uri AuthorityUri = new Uri($"https://login.microsoftonline.com/{TenantId}");
    private static readonly Uri RedirectUrl = new Uri("http://localhost");
    private static readonly string[] Scopes = new[] { "User.Read" };

    private static bool s_useBroker = true;

    private static async Task Main(string[] args)
    {

        [DllImport("user32.dll")]
        static extern nint GetForegroundWindow();

        var brokerOptions = s_useBroker ? 
            new BrokerOptions(BrokerOptions.OperatingSystems.Windows) : 
            new BrokerOptions(BrokerOptions.OperatingSystems.None);

        var pca = PublicClientApplicationBuilder
            .Create(ClientId)
            .WithAuthority(AuthorityUri)
            .WithRedirectUri(RedirectUrl.ToString())
            .WithParentActivityOrWindow(() => GetForegroundWindow())
            .WithBroker(brokerOptions)
            .Build();

        var result = await pca.AcquireTokenInteractive(Scopes)
            .WithPrompt(Prompt.SelectAccount)
            .ExecuteAsync();

        HttpClient client = new HttpClient();
        client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(result.TokenType, result.AccessToken);

        var response2 = await client.GetAsync("https://graph.microsoft.com/oidc/userinfo"); // OIDC - fails with broker, works with browser
        var stringResponse2 = await response2.Content.ReadAsStringAsync();
        Console.Write(stringResponse2);

        var response3 = await client.GetAsync("https://graph.microsoft.com/v1.0/me");  // works with broker and with browser
        var stringResponse3 = await response3.Content.ReadAsStringAsync();
        Console.Write(stringResponse3);

    }
}
marbon87 commented 3 months ago

That the token from the browser is working is mentioned in my first post. Browser is not an option, that's why i wanted to use MSAL4j. Furthermore i do not have any knowledge in .NET.

Do you have a dedicated contact or business support for this kind of issue? We are actually paying a lot for MS Entra...

bgavrilMS commented 3 months ago

Yes, please do escalate this issue via Azure support and via your account manager to get more attention to it.

CC @localden and @ashok672

bgavrilMS commented 2 months ago

I'm editing the issue to make it clear, as it's a pretty long thread.

bgavrilMS commented 2 months ago

@marbon87 - can you convince KeyCloack to call https://graph.microsoft.com/v1.0/me instead of the userinfo endpoint? I think they are similar.

marbon87 commented 2 months ago

https://graph.microsoft.com/v1.0/me does not work either because the response is missing, especiall the sub-claim is missing in the response.

iulico-1 commented 2 months ago

This is a known, recently discovered defect, in eSTS implementation of the protocol used by Windows broker to issue v2 tokens.

We don't have a concrete timeline, but the plan is for eSTS to address this next quarter.

marbon87 commented 3 weeks ago

https://graph.microsoft.com/v1.0/me seems to be working now. Is this uri expected to be used for userinfo?