-
Notifications
You must be signed in to change notification settings - Fork 147
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
base: dev
Are you sure you want to change the base?
Conversation
assertResultNotNull(resultOrganizations); | ||
assertResultNotNull(resultOrganizationsCached); | ||
|
||
assertNotEquals(resultNoOverride.accessToken(), resultOrganizations.accessToken()); |
There was a problem hiding this comment.
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") |
There was a problem hiding this comment.
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:
- go from a tenant specific authority on the CCA to the
lmo/organizations
authority on the request. Usually it's the other way around. - 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
There was a problem hiding this comment.
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.
There was a problem hiding this 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\"}"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- Consider adding this to a common file. It's the standard /token response for S2S
- OBO receives id tokens, refresh tokens etc. The response should reflect that.
- 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")) |
There was a problem hiding this comment.
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\":" + |
There was a problem hiding this comment.
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"))); |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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() { |
There was a problem hiding this comment.
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()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not tested.
There was a problem hiding this 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"))); |
There was a problem hiding this comment.
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.
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.