Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using GraphServiceClient to get refresh tokens when authenticating using UserPasswordCredential in AuthenticationContext

Sincere apologies if I miss something from this post, as I'm at my wits end after reading around for hours.

I'm attempting to write a back-end service (Windows) which will connect to the MS Graph API via Azure AD. I'm using C# to knock up a proof-of-concept but have run into numerous issues with the MS docs, blogs etc. being fairly convoluted and poor quality. They all seem to assume that the consumers of their API are front-end desktop or browser based apps.

Anyway, having managed to get connected to the Graph using the UserPasswordCredential for the service account, I receive a token in the AuthenticationContext response, but no refresh token information. MSDN suggests that the method may return refresh info, but hey ho, it may not.

Furthermore, reading (or attempting to read) this blog post on ADAL 3 (from 2015):

http://www.cloudidentity.com/blog/2015/08/13/adal-3-didnt-return-refresh-tokens-for-5-months-and-nobody-noticed/

Left me confused in terms of how the refresh now works. The article seems to be suggesting that almost magically the tokens are refreshed in the cache for you, but after testing with my POC this is not the case.

I also came across this article from MS which seemed to nail it in the title:

https://msdn.microsoft.com/en-us/office/office365/howto/building-service-apps-in-office-365

However, goes off on a bit of a tangent requiring you to register apps, jump through pop-up hoops permitting access etc. etc.

Since I have my sample running, I've tried scheduling a re-auth to hopefully get a new token 50mins after getting the initial token (which lasts 60mins) however I just get the same token. This means at 60mins 1sec any calls via the client throw an exception (ServiceException where I have to examine the text inside to see the token expiry related info). It doesn't make sense to me to not be able to re-auth and refresh the token to carry on with a more "seamless" use of the client.

Here's a stripped down sample version of my code:

namespace O365GraphTest
{
    using Microsoft.Graph;
    using Microsoft.IdentityModel.Clients.ActiveDirectory;
    using Nito.AsyncEx;
    using System;
    using System.Net.Http;
    using System.Net.Http.Headers;
    using System.Threading.Tasks;
    using System.Linq;
    using System.Threading;
    using System.Security;

    public class Program
    {
        // Just use a single HttpClient under the hood so we don't hit any socket limits.
        private static readonly HttpProvider HttpProvider = new HttpProvider(new HttpClientHandler(), false);

        public static void Main(string[] args)
        {
            try
            {
                AsyncContext.Run(() => MainAsync(args));
            }
            catch (Exception ex)
            {
                Console.Error.WriteLine(ex);
            }
        }

        private static async Task MainAsync(string[] args)
        {
            var tenant = "mytenant.onmicrosoft.com";
            var username = $"test.user@{tenant}";
            var password = "fooooooo";
            var token = await GetAccessToken(username, password, tenant);
            var client = GetClient(token)

            // Example of graph call            
            var skusResult = await client.SubscribedSkus.Request().GetAsync();
        }

        private static async Task<string> GetAccessToken(string username, string password, string tenant = null)
        {
            var authString = tenant == null ?
                $"https://login.microsoftonline.com/common/oauth2/token" :
                $"https://login.microsoftonline.com/{tenant}/oauth2/token";

            var authContext = new AuthenticationContext(authString);
            var creds = new UserPasswordCredential(username, password);
            // Generic client ID
            var clientId = "1950a258-227b-4e31-a9cf-717495945fc2";
            var resource = "https://graph.microsoft.com";

            // NOTE: There's no refresh information here, and re-authing for a token pre-expiry doesn't give a new token.
            var authenticationResult = await authContext.AcquireTokenAsync(resource, clientId, creds);

            return authenticationResult.AccessToken;
        }

        private static GraphServiceClient GetClient(string accessToken, IHttpProvider provider = null)
        {
            var delegateAuthProvider = new DelegateAuthenticationProvider((requestMessage) =>
            {
                requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", accessToken);

                return Task.FromResult(0);
            });

            var graphClient = new GraphServiceClient(delegateAuthProvider, provider ?? HttpProvider);

            return graphClient;
        }
    }
}

If anyone can help me with this, without just saying "do it a different way" I'd be most grateful as there seems to be little discussion about this online (Freenode, IRC) and the blogs are old, the docs on MSDN terse and for specific previous versions of the library etc. e.g.

Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext.AcquireTokenAsync

the refresh method for the AuthenticationContext class has now been removed.

Thanks for any advice.

Peter

UPDATE

Firstly apologies an thanks to @Fei Xue who answered my question, but I couldn't get the gist of what they were saying to begin with.

After chatting with Fei Xue, it does seem that if you keep the auth context around you can use the AcquireTokenSilentAsync to get new tokens going forward, you don't need to handle scheduling any refresh/reauth etc. Just bake in a check in the delegate handler (as to which method you're calling) that is part of getting the client.

Here's the updated version of my sample code that I've tested and it seems to work.

namespace O365GraphTest
{
    using Microsoft.Graph;
    using Microsoft.IdentityModel.Clients.ActiveDirectory;
    using Nito.AsyncEx;
    using System;
    using System.Net.Http;
    using System.Net.Http.Headers;
    using System.Threading.Tasks;

    public class Program
    {
        private const string Resource = "https://graph.microsoft.com";

        // Well known ClientID
        private const string ClientId = "1950a258-227b-4e31-a9cf-717495945fc2";

        private static readonly string Tenant = "mytenant.onmicrosoft.com";

        private static readonly HttpProvider HttpProvider = new HttpProvider(new HttpClientHandler(), false);

        private static readonly AuthenticationContext AuthContext = GetAuthenticationContext(Tenant);

        public static void Main(string[] args)
        {
            try
            {
                AsyncContext.Run(() => MainAsync(args));
            }
            catch (Exception ex)
            {
                Console.Error.WriteLine(ex);
            }
        }

        private static async Task MainAsync(string[] args)
        {
            var userName = $"test.user@{Tenant}";
            var password = "fooooooo";
            var cred = new UserPasswordCredential(userName, password);

            // Get the client and make some graph calls with token expiring delays in between.        
            var client = GetGraphClient(cred);
            var skusResult = await client.SubscribedSkus.Request().GetAsync();
            await Task.Delay(TimeSpan.FromMinutes(65));
            var usersResult = await client.Users.Request().GetAsync();
        }

        private static AuthenticationContext GetAuthenticationContext(string tenant = null)
        {
            var authString = tenant == null ?
                $"https://login.microsoftonline.com/common/oauth2/token" :
                $"https://login.microsoftonline.com/{tenant}/oauth2/token";

            return new AuthenticationContext(authString);
        }

        private static GraphServiceClient GetGraphClient(UserPasswordCredential credential)
        {
            var delegateAuthProvider = new DelegateAuthenticationProvider(async (requestMessage) =>
            {
                var result = AuthContext.TokenCache?.Count > 0 ?
                    await AuthContext.AcquireTokenSilentAsync(Resource, ClientId) :
                    await AuthContext.AcquireTokenAsync(Resource, ClientId, credential);
                requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", result.AccessToken);
            });

            return new GraphServiceClient(delegateAuthProvider, HttpProvider);
        }
    }
}

I was also given 2 MS articles to reference:

Authentication scenarios

Azure AD V2.0 endpoint - client credentials flow

I hope this helps anyone else in the same situation as me, as before this I was pulling my hair out!

like image 849
peteski Avatar asked May 16 '17 17:05

peteski


1 Answers

The GraphServiceClient class is used to operate the Microsoft Graph which is not able to get the access_token or refresh_token.

As the blog mentioned the latest version of azure-activedirectory-library-for-dotnet library doesn't expose the refresh_token to the developers. You can check it from AuthenticationResult.cs class. This library will help to refresh the access_token if the token is expired when we call the method AcquireTokenSilentAsync.

So in your scenario, we should use this method to get the access token for the GraphServiceClient. Then it will always provide the available access token for the GraphServiceClient. Here is the code for your reference:

string authority = "https://login.microsoftonline.com/{tenant}";
string resource = "https://graph.microsoft.com";
string clientId = "";
string userName = "";
string password = "";
UserPasswordCredential userPasswordCredential = new UserPasswordCredential(userName, password);
AuthenticationContext authContext = new AuthenticationContext(authority);
var result = authContext.AcquireTokenAsync(resource, clientId, userPasswordCredential).Result;
var graphserviceClient = new GraphServiceClient(
    new DelegateAuthenticationProvider(
        (requestMessage) =>
        {
            var access_token = authContext.AcquireTokenSilentAsync(resource, clientId).Result.AccessToken;
            requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", access_token);
            return Task.FromResult(0);
        }));

var a = graphserviceClient.Me.Request().GetAsync().Result;

Update

string authority = "https://login.microsoftonline.com/adfei.onmicrosoft.com";
string resrouce = "https://graph.microsoft.com";
string clientId = "";
string userName = "";
string password = "";
UserPasswordCredential userPasswordCredential = new UserPasswordCredential(userName, password);
AuthenticationContext authContext = new AuthenticationContext(authority);
var result = authContext.AcquireTokenAsync(resrouce, clientId, userPasswordCredential).Result;
var graphserviceClient = new GraphServiceClient(
    new DelegateAuthenticationProvider(
        (requestMessage) =>
        {
            var access_token = authContext.AcquireTokenSilentAsync(resrouce, clientId).Result.AccessToken;
            requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", access_token);
            return Task.FromResult(0);
        }));

var a = graphserviceClient.Me.Request().GetAsync().Result;

//simulate the access_token expired to change the access_token
graphserviceClient.AuthenticationProvider=
     new DelegateAuthenticationProvider(
         (requestMessage) =>
         {
             var access_token = "abc";
             requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", access_token);
             return Task.FromResult(0);
         });
var b = graphserviceClient.Me.Request().GetAsync().Result;

Result: enter image description here

like image 166
Fei Xue - MSFT Avatar answered Sep 17 '22 01:09

Fei Xue - MSFT