1

My project is based on this on-behalf-of-flow example.

In my web api I have a non-restricted by [Authorize] method which receives login and password. I also have a restricted method which gets some info from MS Graph API:

[HttpGet]
[Authorize]
[Route("[action]")]
public async Task<IActionResult> Info()
{
    string realAccessToken = await _tokenAcquisition.GetAccessTokenOnBehalfOfUser(HttpContext, scopes).ConfigureAwait(false);
    var userInfoJson = await AzureGraphDataProvider.GetCurrentUserAsync(realAccessToken).ConfigureAwait(false);
...
}

In the non-restricted method I do a post request to Azure and get back information with Access token. Then I try to call directly Graph Api method using this token, but get Unauthorize.

Since on-behalf-of-flow takes User from HttpContext, I get the error above only when I access non-restricted by [authorize] method and call Graph API directly from it using the obtained token.

Current work around is to call a restricted web api method with the obtained token, which then calls MS Graph API, as I guess it initializes HttpContext and on-behalf-of-flow works.

Any better idea, please?

[HttpPost]
[Route("[action]")]
public async Task<IActionResult> GetAzureOAuthData([FromBody]dynamic parameters)
{
    string userName = parameters.userName.ToString();
    string password = parameters.password.ToString();
    using (HttpClient client = new HttpClient())
    {
        var oauthEndpoint = new Uri("https://login.microsoftonline.com/organizations/oauth2/v2.0/token");
        var result = await client.PostAsync(oauthEndpoint, new FormUrlEncodedContent(new[]
        {
            new KeyValuePair<string, string>("client_id", _azureOptions.ClientId),
            new KeyValuePair<string, string>("grant_type", "password"),
            new KeyValuePair<string, string>("username", userName),
            new KeyValuePair<string, string>("password", password),
            new KeyValuePair<string, string>("client_secret", _azureOptions.ClientSecret),
            new KeyValuePair<string, string>("client_info", "1"),
            new KeyValuePair<string, string>("scope",
                "openid offline_access profile api://xxxxxxxxxx-xxxxxxxx-xxxxxxxxx/access_as_user api://xxxxxxxxxx-xxxxxxxx-xxxxxxxxx/access_as_admin"),
        })).ConfigureAwait(false);

        var content = await result.Content.ReadAsStringAsync().ConfigureAwait(false);
        var oar = JsonConvert.DeserializeObject<AzureOAuthResult>(content);

        string[] scopes = { "user.read" };

       // doesn't work, unauthorized
       var currentUserContent = await AzureGraphDataProvider.GetCurrentUserAsync(oar.AccessToken).ConfigureAwait(false);
            // It works, but have to call web api method
            using (HttpClient client2 = new HttpClient())
            {
                client2.DefaultRequestHeaders.Add("Authorization", $"Bearer {oar.AccessToken}");
                var currentUserResult = await client2.GetAsync($"{Request.Scheme}://{Request.Host}/api/users/userinfo");
                var currentUserContent = await currentUserResult.Content.ReadAsStringAsync().ConfigureAwait(false);
            }

        return Ok(...);
    }
}

In AzureGraphDataProvider:

public static async Task<string> GetCurrentUserAsync(string accessToken)
{
    HttpClient client = new HttpClient();
    client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
    HttpResponseMessage response = await client.GetAsync(new Uri("https://graph.microsoft.com/v1.0/me")).ConfigureAwait(false);
    if (response.StatusCode == HttpStatusCode.OK)
    {
        return await response.Content.ReadAsStringAsync().ConfigureAwait(false);
    }

    return string.Empty;
}
Rohit Saigal
  • 9,317
  • 2
  • 20
  • 32
amplifier
  • 1,793
  • 1
  • 21
  • 55

1 Answers1

4

Don't use ROPC password grant and don't collect username/password directly in your api

I see that you're expecting username and password to be supplied directly to your API in order to acquire token using ROPC or password grant. This violates security best practices and also has functional limitations like it does not work with MFA. You can see this SO post for a similar discussion and there are many other resources which will confirm the same for you. Here is an old article but still very detailed. And look at the long list of limitations at the end.

I'm refering to this code you have shared:

public async Task<IActionResult> GetAzureOAuthData([FromBody]dynamic parameters)
{
    string userName = parameters.userName.ToString();
    string password = parameters.password.ToString();
    using (HttpClient client = new HttpClient())
    {
        var oauthEndpoint = new Uri("https://login.microsoftonline.com/organizations/oauth2/v2.0/token");
        var result = await client.PostAsync(oauthEndpoint, new FormUrlEncodedContent(new[]
        {
            new KeyValuePair<string, string>("client_id", _azureOptions.ClientId),
            new KeyValuePair<string, string>("grant_type", "password"),
            new KeyValuePair<string, string>("username", userName),
            new KeyValuePair<string, string>("password", password),

Reason for your non-restricted method not working

You are acquiring a token for your own API using password grant. Then you don't really use On behalf of flow after that to acquire token for Microsoft Graph on behalf of the user who called your API.

So the token is valid for your API, but not for Microsoft Graph API and hence you get the UnAuthorized error.

The other method which you call restricted method, does On-behalf-Of flow by first getting a token for Microsoft Graph API on behalf of the user.

string realAccessToken = await _tokenAcquisition.GetAccessTokenOnBehalfOfUser(HttpContext, scopes).ConfigureAwait(false);

Better Ideas as you ask for it

The client application which calls your API, should make use of delegated permissions and OAuth flows like authorization code grant flow or others depending on your scenario to acquire an access token for your API.

API can then make use of On-Behalf-Of flow, like you do in restricted method to get the token required by Microsoft Graph API.

Rohit Saigal
  • 9,317
  • 2
  • 20
  • 32