Skip to content
This repository has been archived by the owner on Feb 7, 2020. It is now read-only.

Assertion is not within its valid time range. #25

Closed
bdebaere opened this issue May 16, 2018 · 16 comments
Closed

Assertion is not within its valid time range. #25

bdebaere opened this issue May 16, 2018 · 16 comments

Comments

@bdebaere
Copy link

Using code based on your TodoListController.cs where the only differences are no token cache and the way I am retrieving the first access token since Azure itself does the authentication (configured in the Azure portal) I am sometimes receiving the following error: AADSTS50013: Assertion is not within its valid time range. I think always after leaving the session open for a while. If a new session is started, for example with incognito mode, then it will work again.

These differences in of themselves should to my knowledge not cause an error like this. I've also found very little information online about this. I am using Microsoft.IdentityModel.Clients.ActiveDirectory version 3.19.4.

Can you please nudge me in the right direction for solving this?

This is my current code to get an on behalf token based on your TodoListController.cs:

        protected async Task<string> GetOnBehalfOfToken(string tenant, string clientId, string appKey, string resourceId)
        {
            // Get access token from header.
            string accessToken = Request.Headers.FirstOrDefault(fod => fod.Key == "X-MS-TOKEN-AAD-ID-TOKEN").Value?.FirstOrDefault();
            if (accessToken == null)
            {
                throw new Exception("No access token found.");
            }

            // Get username from claims.
            string userName = ClaimsPrincipal.Current.FindFirst(ClaimTypes.Upn) != null ? ClaimsPrincipal.Current.FindFirst(ClaimTypes.Upn).Value : ClaimsPrincipal.Current.FindFirst(ClaimTypes.Email).Value;
            if (userName == null)
            {
                throw new Exception("No username found.");
            }

            // Get on behalf of token.
            UserAssertion userAssertion = new UserAssertion(accessToken, "urn:ietf:params:oauth:grant-type:jwt-bearer", userName);
            const string aadInstance = "https://login.microsoftonline.com/";
            string authority = aadInstance.UrlCombine(tenant);
            AuthenticationContext authenticationContext = new AuthenticationContext(authority);
            ClientCredential clientCredential = new ClientCredential(clientId, appKey);
            AuthenticationResult authenticationResult = await
                authenticationContext.AcquireTokenAsync(resourceId, clientCredential, userAssertion);

            return authenticationResult.AccessToken;
        }
@kalyankrishna1
Copy link
Contributor

The access token is valid for an hour and the ADAL library tucks away a refresh_token in the cache to renew it automatically every time it expires. You can read about the various tokens and their expiration times here (https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-v2-tokens#token-lifetimes).
I believe the access token you are trying to use to construct an UserAssertion is an expired one. And without the cache, the library has no means to get a fresh one for you.

@bdebaere
Copy link
Author

@kalyankrishna1 Thank you for your response. I will look into adding the cache to the code and see if that resolves the issue.

@bdebaere bdebaere reopened this Jul 13, 2018
@bdebaere
Copy link
Author

bdebaere commented Jul 13, 2018

@kalyankrishna1 I'm still having this issue. Even with a cache. Do note that I'm retrieving the X-MS-TOKEN-AAD-ID-TOKEN header to build the on behalf of token and not the X-MS-TOKEN-AAD-ACCESS-TOKEN because the latter header is not present in the request.

@jmprieur
Copy link
Contributor

@bdebaere : would there be a dis-synchronization between the clock of the computer running the sample and the internet time?

@kalyankrishna1
Copy link
Contributor

Email us the fiddler trace if possible.

@bdebaere
Copy link
Author

bdebaere commented Aug 7, 2018

@jmprieur The API is run in Azure. The API is accessed on different kinds of computers. There is a time difference between Azure and our computers here of around 7 hours.

@jmprieur
Copy link
Contributor

jmprieur commented Aug 7, 2018

Thanks for the precisions, @bdebaere. I'll try to repro and understand.

@jmprieur
Copy link
Contributor

jmprieur commented Aug 8, 2018

@GeoK in case you have an idea

@bdebaere
Copy link
Author

@jmprieur I am able to reproduce this with the following code:

Use this to get an access token:

        public static async Task<string> GetAccessToken(
            string userName,
            string password)
        {
            var formContent = new FormUrlEncodedContent(new[]
            {
                new KeyValuePair<string, string>("resource", "hidden"),
                new KeyValuePair<string, string>("client_secret", "hidden"),
                new KeyValuePair<string, string>("username", userName),
                new KeyValuePair<string, string>("password", password),
                new KeyValuePair<string, string>("client_id", "hidden"),
                new KeyValuePair<string, string>("grant_type", "password")
            });

            var httpClient = new HttpClient();
            var result = await httpClient.PostAsync(new Uri("https://login.microsoftonline.com/hidden.onmicrosoft.com/oauth2/token"), formContent);
            var content = await result.Content.ReadAsStringAsync();
            var accessToken = JObject.Parse(content)["access_token"].ToString();

            return accessToken;
        }

Use the token you just got from the function above with this function below:

        protected async Task<string> GetOnBehalfOfToken(string tenant, string clientId, string appKey, string resourceId)
        {
            const string aadInstance = "https://login.microsoftonline.com/";
            var accessToken = "tokenYouGotFromFunction";
            var userName = "hidden";

            // Get on behalf of token.
            var userAssertion = new UserAssertion(accessToken, "urn:ietf:params:oauth:grant-type:jwt-bearer", userName);
            var authority = aadInstance.UrlCombine(tenant);
            var authenticationContext = new AuthenticationContext(authority, new FileCache());
            var clientCredential = new ClientCredential(clientId, appKey);
            var authenticationResult =
                await authenticationContext.AcquireTokenAsync(resourceId, clientCredential, userAssertion);

            return authenticationResult.AccessToken;
        }

If you keep using the token as pasted string it will work for about an hour. Then it will throw the error: "Error validating credentials. AADSTS50013: Assertion is not within its valid time range.".

This is my crude FileCache:

    public class FileCache : TokenCache
    {
        public string CacheFilePath;
        private static readonly object FileLock = new object();

        // Initializes the cache against a local file.
        // If the file is already present, it loads its content in the ADAL cache
        public FileCache()
        {
            string filePath = $@"{AppDomain.CurrentDomain.BaseDirectory}\TokenCache.dat";
            CacheFilePath = filePath;
            this.AfterAccess = AfterAccessNotification;
            this.BeforeAccess = BeforeAccessNotification;
            lock (FileLock)
            {
                //this.Deserialize(File.Exists(CacheFilePath) ? ProtectedData.Unprotect(File.ReadAllBytes(CacheFilePath), null, DataProtectionScope.CurrentUser) : null);
                //this.Deserialize(File.Exists(CacheFilePath) ? ProtectedData.Unprotect(File.ReadAllBytes(CacheFilePath), null, DataProtectionScope.LocalMachine) : null);
                this.Deserialize(File.Exists(CacheFilePath) ? File.ReadAllBytes(CacheFilePath) : null);
            }
        }

        // Empties the persistent store.
        public override void Clear()
        {
            base.Clear();
            File.Delete(CacheFilePath);
        }

        // Triggered right before ADAL needs to access the cache.
        // Reload the cache from the persistent store in case it changed since the last access.
        void BeforeAccessNotification(TokenCacheNotificationArgs args)
        {
            lock (FileLock)
            {
                //this.Deserialize(File.Exists(CacheFilePath) ? ProtectedData.Unprotect(File.ReadAllBytes(CacheFilePath), null, DataProtectionScope.CurrentUser) : null);
                //this.Deserialize(File.Exists(CacheFilePath) ? ProtectedData.Unprotect(File.ReadAllBytes(CacheFilePath), null, DataProtectionScope.LocalMachine) : null);
                this.Deserialize(File.Exists(CacheFilePath) ? File.ReadAllBytes(CacheFilePath) : null);
            }
        }

        // Triggered right after ADAL accessed the cache.
        void AfterAccessNotification(TokenCacheNotificationArgs args)
        {
            // if the access operation resulted in a cache update
            if (this.HasStateChanged)
            {
                lock (FileLock)
                {
                    // reflect changes in the persistent store
                    //File.WriteAllBytes(CacheFilePath, ProtectedData.Protect(this.Serialize(), null, DataProtectionScope.CurrentUser));
                    //File.WriteAllBytes(CacheFilePath, ProtectedData.Protect(this.Serialize(), null, DataProtectionScope.LocalMachine));
                    File.WriteAllBytes(CacheFilePath, this.Serialize());
                    // once the write operation took place, restore the HasStateChanged bit to false
                    this.HasStateChanged = false;
                }
            }
        }
    }

@kalyankrishna1
Copy link
Contributor

You need to use the ADAL library and one of its AcquireToken() overloads for your file cache to work

@jmprieur
Copy link
Contributor

@bdebaere : indeed, @kalyankrishna1 is right: the authentication libraries (ADAL.NET and MSAL.NET) refresh the token by themselves.

Which is your scenario ? Are you writing a Web API? or a Web App? or a desktop application?

Finally if you are writing a web app or a Web API you should not use Username password as this should be only called for Desktop/Mobile applications. BTW ADAL.NET can do it by itself (See Using Username/password )

Did you have a look at ADAL.NET conceptual documentation where we explain all the ways to acquire a token?

@bdebaere
Copy link
Author

bdebaere commented Aug 31, 2018

@jmprieur The scenario is a web API with authentication configured in the Azure portal. The access token is sent around by the Azure wrapper around my API. I'm never calling AcquireTokenAsync() to get the first access token. Not any configuration is done to receive the first token. It is given to me by Azure.

Are you telling me this scenario is not supported?
Or are you telling me I have to call AcquireTokenAsync in my API even though I have picked Azure to handle authentication? If so, which AcquireTokenAsync would be suitable then?

@jmprieur
Copy link
Contributor

jmprieur commented Aug 31, 2018

Getting the token and then using the OBO flow is supported but you need to cache things.
The bit I don't understand @bdebaere is your GetAccessToken(string userName, string password) method in your comment above. Also I see that you are using a file cache. where is the file written? Usually Web apis use other kind of caches? See Token cache serialization in Web APIs

@bdebaere
Copy link
Author

bdebaere commented Aug 31, 2018

@jmprieur Allow me to resketch the situation

  1. I created a web API from scratch without any authentication configured. So no Startup.Auth.cs.
  2. I created a web API resource in the Azure portal with nothing special configured.
  3. In the created web API resource in the Azure portal I configured authentication to express.
    image
  4. In the express configured app registration I added the required permissions for the resource I'm trying to access on behalf of. In this case AzureAnalysisServices.
    image
  5. In my web API I get the accesstoken with the following code: string accessToken = Request.Headers.FirstOrDefault(fod => fod.Key == "X-MS-TOKEN-AAD-ID-TOKEN").Value?.FirstOrDefault();. I do this because BootstrapContext is empty.
  6. The rest of the code is the same as the sample. I fill in my web API id as clientId and generate a key in the app registration to use as appKey.
            string userName = ClaimsPrincipal.Current.FindFirst(ClaimTypes.Upn) != null ? ClaimsPrincipal.Current.FindFirst(ClaimTypes.Upn).Value : ClaimsPrincipal.Current.FindFirst(ClaimTypes.Email).Value;
            UserAssertion userAssertion = new UserAssertion(accessToken, "urn:ietf:params:oauth:grant-type:jwt-bearer", userName);
            const string aadInstance = "https://login.microsoftonline.com/";
            string authority = aadInstance.UrlCombine(tenant);
            AuthenticationContext authenticationContext = new AuthenticationContext(authority, new FileCache());
            ClientCredential clientCredential = new ClientCredential(clientId, appKey);
            AuthenticationResult authenticationResult = await
                authenticationContext.AcquireTokenAsync(resourceId, clientCredential, userAssertion);

Where exactly in this process am I going wrong?

The file cache I'm using is just a test. I realize that a db cache is more fitting, but this is just a proof of concept at this moment. I can see that the file cache is being written to, but as soon as it comes to refreshing the token, it no longer works and I get the error I mentioned before. The GetAccessToken function is not used in my web API or anywhere else, it was merely used to display a test for you of when the error is thrown.

If I access my web API through http://myapi.azurewebsites.com/FunctionThatRequiresOnBehalfOf then it works for an hour because the token does not need refreshing, but as soon as it does it doesn't seem to realize that it needs to do so when asking the on behalf of token.

I'm really trying hard here to understand where the issue lies and what I can do to solve it. Please help me understand.

@jmprieur
Copy link
Contributor

Thanks for your detailed explanation. @bdebaere. This definitively clarifies.
The issue you have is that the token used to call your Web API has expired (the token in the user assertion), and therefore Azure AD does not exchange this token against another token to call your downstream API.

Did you think of securing your Web API directly with an [Authorize] attribute? instead of using the app service authentication? like in https://github.com/Azure-Samples/active-directory-dotnet-webapi-onbehalfof

@bdebaere
Copy link
Author

@jmprieur Yes, this was the obvious next route to try, but I wanted to confirm if there wasn't an option anyway. This way I can also relay this information to my colleagues.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants