Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Access Azure Graph API on behalf of application rather than user

Following on from my previous question: Group claims with Azure AD and OAuth2 implicit grant in ADAL JS, I've have things set up so users can authenticate using Azure/ADAL JS and then I use the their token to access the Azure Graph API on behalf of that user. This works well and I'm able to get their user and group information.

However, we now have a use case where another system will log into our application in the application's context rather than as an individual user. I don't know if I'm doing it correctly but I've got a second Azure AD Application with a client key that authenticates by requesting a token from the AAD API. I can get a token and pass that to our application. However, I can no longer use that token to access the Azure Graph API on behalf of that user (which is now an application).

Firstly, is this actually possible to do or am I attempting the impossible?

If it is possible, what do I have to do differently to get it to work? Is it just permissions in the Azure application or do I need to do it differently in code?

The code I use for accessing the Graph API on behalf of a user (with exception handling and null checks removed) is:

var authContext = new AuthenticationContext("https://login.microsoftonline.com/common");
var clientCredential = new ClientCredential("clientId", "key");
var userAssertion = new UserAssertion(((BootstrapContext)identity.BootstrapContext).Token);

var result = await authContext.AcquireTokenAsync("https://graph.windows.net", clientCredential, userAssertion);
return result.AccessToken;

The exception I get is AADSTS50034: To sign into this application the account must be added to the {directory ID of my main application} directory with an error of invalid_grant.

I can't seem to figure out what I've done wrong as I believe all the Azure applications are configured correctly, well they are for user authentication at least. I also applied the same application and delegated permissions to my second Azure AD (client) application that other directories need for user auth.

Any help would be appreciated.

Update: system overview/configuration

So it seems that I didn't provide enough context around how the system is configured so let me attempt to address that here.

We have an enterprise SaaS application running in Azure (let's call this our application). It has an "application" in Azure AD (let's call this our AAD application to avoid confusion). This is a multi-tenant AAD application and users authenticate using OAuth2 via AAD.

Being an enterprise application, our customers all have their own Azure AD (which may or may not be synced to an on-premise AD) (let's call this their AAD). When configuring their system to work with our application, we have a Global Administrator from their AAD grant our AAD application consent to the following permissions for Windows Azure Active Directory (using the admin consent grant):

  • Application permissions:
    • Read directory data
  • Delegated permissions:
    • Access the directory as the signed-in user
    • Read directory data
    • Sign in and read user profile

When the users browse to our application, they will be redirected to Azure for authentication. After being authenticated (whether by their AAD or via an on-premise AD connected to their AAD), any API calls made via the web app will include a Bearer token. The first time we see each Bearer token, we use it to get a new token for the Azure Graph API (On Behalf Of the user) and query the Graph API for user details and group memberships. This all works when users authenticate via a UI with their own user account.

What we are looking to do now allow our customers to have another downstream application (their downstream application) get its own Bearer token to use our application. This time there is no user so we're looking at the client credentials grant flow. To do this, our customer now has their own AAD application (their AAD application) which is in their AAD (which has already been granted consent as above). Their downstream application can get a Bearer token to access our application. However, when we try to get a token for the Azure Graph API (On Behalf Of their downstream application), it fails with the error message I pasted above.

Update 2

Another member of my team has done some investigation and these are his findings.

I've been through the process manually by looking at what the SDK does behind the scenes and doing all the requests manually to better allow it to be reproduce what we are doing.

So the calling service (which is a web application setup inside a Azure Active Directory) obtains an OAuth token from Azure, i.e.

POST https://login.microsoftonline.com/<tenant-id>/oauth2/token

grant_type      client_credentials
client_id       (the client ID of the calling service application in the AD)
client secret   (the key configured in the calling service application in the AD)
resource        (the client ID of our web service)

And we get a valid token which contains a appid, tid, oid, iss, etc. but no name, upn, etc. (since it is not a user but an application).

So then we want to look up the details of the service principal from the Graph API, and we request to get a Graph API token on behalf of the application, i.e.

POST https://login.microsoftonline.com/common/oauth2/token

grant_type            urn:ietf:params:oauth:grant-type:jwt-bearer
client_id             (our web service application client ID)
client_secret         (our web service application client key)
resource              https://graph.windows.net
assertion             (the token that was sent by the calling client service, obtained by the process above)
requested_token_use   on_behalf_of
scope                 openid

Which is basically what happens when we call the following code (extracted from the SDK code)

var authContext = new AuthenticationContext("https://login.microsoftonline.com/common");
var clientCredential = new ClientCredential("clientId", "key");
var userAssertion = new UserAssertion(((BootstrapContext)identity.BootstrapContext).Token);

var result = await authContext.AcquireTokenAsync("https://graph.windows.net", clientCredential, userAssertion);
return result.AccessToken;

All it returns is:

{
  "error": "invalid_grant",
  "error_description": "AADSTS50034: To sign into this application the account must be added to the <customer tenant id> directory.\r\nTrace ID: <removed>\r\nCorrelation ID: <removed>\r\nTimestamp: 2015-10-08 06:37:58Z",
  "error_codes": [ 50034 ],
  "timestamp": "2015-10-08 06:37:58Z",
  "trace_id": "<removed>",
  "correlation_id": "<removed>"
}

So I guess the question is, can we get a graph API token on behalf of a caller with a bearer token, if the caller is an web/native application and not an actual user?

like image 722
Richard Pickett Avatar asked Sep 01 '15 13:09

Richard Pickett


1 Answers

There's a way to log in the Microsoft Graph API without user credentials. You can setup a ClientCredential with a Client ID and the Client Secret. With that ClientCredential you can authenticate an AuthenticationContext and retrieve an access token. The access token gives you access to the Graph API.

Code Sample:

string aadInstance = "https://login.microsoftonline.com/{0}";
string tenant = "<your-tenant>.onmicrosoft.com";
string clientId = "<your-clientId>";
string appKey = "<your-application-key>"; // Or Client Secret
string resourceId = "https://graph.microsoft.com/";
string authority = String.Format(CultureInfo.InvariantCulture, aadInstance, tenant);

var authContext = new AuthenticationContext(authority);
var clientCredential = new ClientCredential(clientId, appKey);
var result = await authContext.AcquireTokenAsync(resourceId, clientCredential);
string response;

using (HttpClient client = new HttpClient())
{
    client.BaseAddress = new Uri("https://graph.microsoft.com");
    client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken);
    client.DefaultRequestHeaders
        .Accept
        .Add(new MediaTypeWithQualityHeaderValue("application/json"));

    var resultApi = await client.GetAsync("/v1.0/users");
    response = await resultApi.Content.ReadAsStringAsync();
}

Hope this helps!

like image 178
koelkastfilosoof Avatar answered Nov 15 '22 04:11

koelkastfilosoof