For an integration test I have an authorized .NET Core 2.2 Controller that is calling another authorized controller (different project) or external api (like Microsoft Graph).
Both apis are authenticated against the Azure AD. In all the controller actions we need the authenticated user. We can get in the first api by getting a token based on the username and password (grant_type=password). When the call continues to the second api, it breaks because of an interactive login prompt (We use ADAL).
Normally, the user authenticates with open id connect, we then have the authentication code and get the accesstoken + refresh token with the authentication code. With the refresh token we can get an access token for the second api.
We created a small sample project with default Values Controllers to explain our problem.
Get access token before calling the first api with native app registration:
public static async Task<string> AcquireTokenAsync(string username, string password)
{
var aadInstance = "https://login.windows.net/{0}";
var tenantId = "put id here";
var authority = string.Format(aadInstance, tenantId);
var clientId = "clientid here";
var resource = "put resource here";
var client = new HttpClient();
var tokenEndpoint = $"https://login.microsoftonline.com/{tenantId}/oauth2/token";
var body = $"resource={resource}&client_id={clientId}&grant_type=password&username={username}&password={password}";
var stringContent = new StringContent(body, Encoding.UTF8, "application/x-www-form-urlencoded");
var result = await client.PostAsync(tokenEndpoint, stringContent).ContinueWith((response) =>
{
return response.Result.Content.ReadAsStringAsync().Result;
});
JObject jobject = JObject.Parse(result);
var token = jobject["access_token"].Value<string>();
return token;
}
First API:
[Authorize]
[HttpGet]
public async Task<IActionResult> Get()
{
string name = User.Identity.Name;
var result = await AcquireTokenSilentWithImpersonationAsync();
string BaseUrl = "https://localhost:44356/";
var client = new HttpClient
{
BaseAddress = new Uri(BaseUrl)
};
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken);
var url = "api/values";
HttpResponseMessage response = await client.GetAsync(url);
switch (response.StatusCode)
{
case HttpStatusCode.OK:
int x = 1;
break;
default:
throw new HttpRequestException($"Error - {response.StatusCode} in response with message '{response.RequestMessage}'");
}
return Ok();
}
private const string BackendResource = "Second api resource here";
/// <summary>
/// For more information: https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-devhowto-adal-error-handling
/// </summary>
/// <returns></returns>
public async Task<AuthenticationResult> AcquireTokenSilentWithImpersonationAsync()
{
const string ClientId = "client id of first api here";
const string ClientSecret = "secret of first api here";
ClientCredential credential = new ClientCredential(ClientId, ClientSecret);
string userObjectId = _httpContextAccessor.HttpContext.User.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier")?.Value;
var authContext = GetAuthenticationContext(userObjectId);
AuthenticationResult authResult = null;
try
{
authResult = await authContext.AcquireTokenSilentAsync(BackendResource, credential, new UserIdentifier(userObjectId, UserIdentifierType.UniqueId));
}
catch (AdalSilentTokenAcquisitionException ex)
{
// Exception: AdalSilentTokenAcquisitionException
// Caused when there are no tokens in the cache or a required refresh failed.
// Action: Case 1, resolvable with an interactive request.
try
{
authResult = await authContext.AcquireTokenAsync(BackendResource, ClientId, new Uri("https://backurl.org"), new PlatformParameters(), new UserIdentifier(userObjectId, UserIdentifierType.UniqueId));
}
catch (Exception exs)
{
throw;
}
}
catch (AdalException e)
{
// Exception: AdalException
// Represents a library exception generated by ADAL .NET.
// e.ErrorCode contains the error code.
// Action: Case 2, not resolvable with an interactive request.
// Attempt retry after a timed interval or user action.
// Example Error: network_not_available, default case.
throw;
}
return authResult;
}
Second api:
[Authorize]
[HttpGet]
public ActionResult<IEnumerable<string>> Get()
{
string name = User.Identity.Name;
return new string[] { "value1", "value2" };
}