Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pass optional tenant override to internal silent call #886

Open
wants to merge 3 commits into
base: dev
Choose a base branch
from

Conversation

Avery-Dunn
Copy link
Collaborator

Fixes an issue described in #881. Essentially, there is an API at the request level which overrides the tenant set at the application level.

However, in some confidential flows we internally make a silent call with a new SilentParameters object which did not receive the tenant that may have been set with that API.

This PR fixes the two places where that API wasn't getting properly passed to that internal silent call, adds a new test for that behavior, and makes a few unrelated fixes/improvements to some integration tests.

assertResultNotNull(resultOrganizations);
assertResultNotNull(resultOrganizationsCached);

assertNotEquals(resultNoOverride.accessToken(), resultOrganizations.accessToken());
Copy link
Member

Choose a reason for hiding this comment

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

nit: you really should get that improvement of exposing a fromCache flag in the auth result going. This string comparison is not great. ESTS-R for example caches tokens.

IAuthenticationResult resultOrganizations = cca.acquireToken(OnBehalfOfParameters.builder(
Collections.singleton(cfg.graphDefaultScope()),
new UserAssertion(accessToken))
.tenant("organizations")
Copy link
Member

Choose a reason for hiding this comment

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

I am not sure I agree with this test. Afaik MSAL will use the tenant ID from the response, not from the request, so it's not that clear what this test does. It's also showcases 2 anti-patterns:

  1. go from a tenant specific authority on the CCA to the lmo/organizations authority on the request. Usually it's the other way around.
  2. OBO with "/common" or "/organizations". This is wrong, OBO should always be tenanted. It should use the client token's tid claim. Otherwise you introduce a bug where guest users can't login. See https://learn.microsoft.com/en-us/entra/msal/dotnet/acquiring-tokens/web-apps-apis/on-behalf-of-flow#important-note-on-on-behalf-of-obo-flow-with-guest-users

My recommendation is to rely on unit tests for this one:

  • app authority is { "lmo/organizations", "lmo/T1", "lmo/T2" }
  • req tenant is {T1, T2}
  • flow { OBO, client_creds, silent}

So there are quite a few unit tests (3x2x3), but mostly repetitive.

Then Assert on the correct MSAL behavior i.e. token from cache or from the correctly tenanted token endpoint

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Agreed that unit tests would be better, originally I just did an integration test since there were ones to copy from but we didn't have any OBO unit tests.

In the latest commit I replaced this integration test with a couple of unit tests. It's not as many as you suggest, but they should show the correct behavior by checking for the correct number of HTTPClient calls, the token cache size, etc., depending on whether the tenant was set at the request level or not.

Copy link
Member

@bgavrilMS bgavrilMS left a comment

Choose a reason for hiding this comment

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

Needs a unit test


private String getSuccessfulResponse(String accessToken) {
return "{\"access_token\":\""+accessToken+"\",\"expires_in\": \""+ 60*60*1000 +"\",\"token_type\":" +
"\"Bearer\",\"client_id\":\"client_id\",\"Content-Type\":\"text/html; charset=utf-8\"}";
Copy link
Member

Choose a reason for hiding this comment

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

  1. Consider adding this to a common file. It's the standard /token response for S2S
  2. OBO receives id tokens, refresh tokens etc. The response should reflect that.
  3. I am not aware of the COntent-Type being part of the response content. It may be a header?

when(httpClientMock.send(any(HttpRequest.class))).thenReturn(expectedResponse(200, getSuccessfulResponse("token")));

ConfidentialClientApplication cca =
ConfidentialClientApplication.builder("clientId", ClientCredentialFactory.createFromSecret("password"))
Copy link
Member

Choose a reason for hiding this comment

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

Consider using some constants for ClientID.

class OnBehalfOfTests {

private String getSuccessfulResponse(String accessToken) {
return "{\"access_token\":\""+accessToken+"\",\"expires_in\": \""+ 60*60*1000 +"\",\"token_type\":" +
Copy link
Member

Choose a reason for hiding this comment

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

You also need to add a client_info, that is part of all user token protocols.

.httpClient(httpClientMock)
.build();

when(httpClientMock.send(any(HttpRequest.class))).thenReturn(expectedResponse(200, getSuccessfulResponse("appTenantToken")));
Copy link
Member

Choose a reason for hiding this comment

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

THis is a great path forward for tests. Consider making a separate class with all this logic. It should the standard way of doing tests and little by little all tests should be re-written to use this.

ConfidentialClientApplication cca =
ConfidentialClientApplication.builder("clientId", ClientCredentialFactory.createFromSecret("password"))
.authority("https://login.microsoftonline.com/tenant")
.instanceDiscovery(false)
Copy link
Member

Choose a reason for hiding this comment

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

ok for now, but the http mock can easily handle this as well.

IAuthenticationResult result = cca.acquireToken(parameters).get();
IAuthenticationResult result2 = cca.acquireToken(parameters).get();

//OBO flow should perform an internal cache lookup, so similar parameters should only cause one HTTP client call
Copy link
Member

Choose a reason for hiding this comment

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

So do you confirm that the lookup is done by assertion_hash in this case?

@@ -27,4 +38,21 @@ static void deleteFileContent(Class<?> classInstance, String resource)
fileWriter.write("");
fileWriter.close();
}

static String generateToken() {
Copy link
Member

Choose a reason for hiding this comment

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

This is fine. Other MSALs just create a json to represent the JWK. MSALs do not validate headers nor signatures, so only the payload matters.

You don't really need a "proper" token for OBO. MSAL never looks into it.

You will need a proper id token though.

@@ -26,6 +26,7 @@ AuthenticationResult execute() throws Exception {
SilentParameters parameters = SilentParameters
.builder(this.clientCredentialRequest.parameters.scopes())
.claims(this.clientCredentialRequest.parameters.claims())
.tenant(this.clientCredentialRequest.parameters.tenant())
Copy link
Member

Choose a reason for hiding this comment

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

This is not tested.

Copy link
Member

@bgavrilMS bgavrilMS left a comment

Choose a reason for hiding this comment

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

More testing is needed.

.httpClient(httpClientMock)
.build();

when(httpClientMock.send(any(HttpRequest.class))).thenReturn(expectedResponse(200, getSuccessfulResponse("appTenantToken")));
Copy link
Member

Choose a reason for hiding this comment

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

You should only return that response once, not all the time, i.e. assert that only 1 call to the token endpoint is made.

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.

2 participants