-
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?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
// Copyright (c) Microsoft Corporation. All rights reserved. | ||
// Licensed under the MIT License. | ||
|
||
package com.microsoft.aad.msal4j; | ||
|
||
import java.util.Collections; | ||
import java.util.HashMap; | ||
import java.util.List; | ||
import java.util.Map; | ||
|
||
import org.junit.jupiter.api.Test; | ||
import org.junit.jupiter.api.extension.ExtendWith; | ||
import org.mockito.junit.jupiter.MockitoExtension; | ||
import static org.junit.jupiter.api.Assertions.assertEquals; | ||
import static org.junit.jupiter.api.Assertions.assertNotEquals; | ||
import static org.mockito.ArgumentMatchers.any; | ||
import static org.mockito.Mockito.*; | ||
import static org.mockito.Mockito.times; | ||
|
||
@ExtendWith(MockitoExtension.class) | ||
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 commentThe 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. |
||
"\"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 commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
} | ||
|
||
private HttpResponse expectedResponse(int statusCode, String response) { | ||
Map<String, List<String>> headers = new HashMap<String, List<String>>(); | ||
headers.put("Content-Type", Collections.singletonList("application/json")); | ||
|
||
HttpResponse httpResponse = new HttpResponse(); | ||
httpResponse.statusCode(statusCode); | ||
httpResponse.body(response); | ||
httpResponse.addHeaders(headers); | ||
|
||
return httpResponse; | ||
} | ||
|
||
@Test | ||
void OnBehalfOf_InternalCacheLookup_Success() throws Exception { | ||
DefaultHttpClient httpClientMock = mock(DefaultHttpClient.class); | ||
|
||
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 commentThe reason will be displayed to describe this comment to others. Learn more. Consider using some constants for ClientID. |
||
.authority("https://login.microsoftonline.com/tenant/") | ||
.instanceDiscovery(false) | ||
.validateAuthority(false) | ||
.httpClient(httpClientMock) | ||
.build(); | ||
|
||
OnBehalfOfParameters parameters = OnBehalfOfParameters.builder(Collections.singleton("scopes"), new UserAssertion(TestHelper.signedToken)).build(); | ||
|
||
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 commentThe 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? |
||
assertEquals(result.accessToken(), result2.accessToken()); | ||
verify(httpClientMock, times(1)).send(any()); | ||
} | ||
|
||
@Test | ||
void OnBehalfOf_TenantOverride() throws Exception { | ||
DefaultHttpClient httpClientMock = mock(DefaultHttpClient.class); | ||
|
||
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 commentThe 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. |
||
.validateAuthority(false) | ||
.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 commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
OnBehalfOfParameters parameters = OnBehalfOfParameters.builder(Collections.singleton("scopes"), new UserAssertion(TestHelper.signedToken)).build(); | ||
|
||
//The two acquireToken calls have the same parameters and should only cause one call from the HTTP client | ||
IAuthenticationResult resultAppLevelTenant = cca.acquireToken(parameters).get(); | ||
cca.acquireToken(parameters).get(); | ||
assertEquals(1, cca.tokenCache.accessTokens.size()); | ||
verify(httpClientMock, times(1)).send(any()); | ||
|
||
when(httpClientMock.send(any(HttpRequest.class))).thenReturn(expectedResponse(200, getSuccessfulResponse("requestTenantToken"))); | ||
parameters = OnBehalfOfParameters.builder(Collections.singleton("scopes"), new UserAssertion(TestHelper.signedToken)).tenant("otherTenant").build(); | ||
|
||
//Overriding the tenant parameter in the request should lead to a new token call being made, but followup calls should not | ||
IAuthenticationResult resultRequestLevelTenant = cca.acquireToken(parameters).get(); | ||
cca.acquireToken(parameters).get(); | ||
assertEquals(2, cca.tokenCache.accessTokens.size()); | ||
verify(httpClientMock, times(2)).send(any()); | ||
assertNotEquals(resultAppLevelTenant.accessToken(), resultRequestLevelTenant.accessToken()); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,6 +3,11 @@ | |
|
||
package com.microsoft.aad.msal4j; | ||
|
||
import com.nimbusds.jose.*; | ||
import com.nimbusds.jose.crypto.RSASSASigner; | ||
import com.nimbusds.jose.jwk.RSAKey; | ||
import com.nimbusds.jose.jwk.gen.RSAKeyGenerator; | ||
|
||
import java.io.File; | ||
import java.io.FileWriter; | ||
import java.io.IOException; | ||
|
@@ -12,10 +17,16 @@ | |
|
||
class TestHelper { | ||
|
||
static String readResource(Class<?> classInstance, String resource) throws IOException, URISyntaxException { | ||
return new String( | ||
Files.readAllBytes( | ||
Paths.get(classInstance.getResource(resource).toURI()))); | ||
//Signed JWT which should be enough to pass the parsing/validation in the library, useful if a unit test needs an | ||
// assertion in a request or token in a response but that is not the focus of the test | ||
static String signedToken = generateToken(); | ||
|
||
static String readResource(Class<?> classInstance, String resource) { | ||
try { | ||
return new String(Files.readAllBytes(Paths.get(classInstance.getResource(resource).toURI()))); | ||
} catch (IOException | URISyntaxException e) { | ||
throw new RuntimeException(e); | ||
} | ||
} | ||
|
||
static void deleteFileContent(Class<?> classInstance, String resource) | ||
|
@@ -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 commentThe 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. |
||
try { | ||
RSAKey rsaJWK = new RSAKeyGenerator(2048) | ||
.keyID("kid") | ||
.generate(); | ||
JWSObject jwsObject = new JWSObject( | ||
new JWSHeader.Builder(JWSAlgorithm.RS256).keyID(rsaJWK.getKeyID()).build(), | ||
new Payload("payload")); | ||
|
||
jwsObject.sign(new RSASSASigner(rsaJWK)); | ||
|
||
return jwsObject.serialize(); | ||
} catch (JOSEException e) { | ||
throw new RuntimeException(e); | ||
} | ||
} | ||
} |
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.