1

I'm using OAuth and O365 single sign on in my web app. When the application starts the user is prompted to log in using SSO. Getting a token works and log-in is successful. Once the user is logged in, they interact with Graph using a graph client that builds a ConfidentialClientApplication and accesses the Session Token Store. That works fine too. But after 45 minutes to an hour, that same client call fails. There is an object in the token store but when CCA calls GetAccountsAsync(), it always returns 0, so AcquireTokenSilentAsync fails. Have I failed to store some relevant information in the token store? What am I doing wrong here and how do I fix it? I've tried re-issuing the challenge and then addressing the token store again, but the result is the same: despite an object in the store, GetAccountsAsync returns 0 accounts.

I'm wondering if it's something to do with the TokenStore examples that I've been basing mine off of. I've tried storing the token in Session and in Cache, and they both work when the user first authenticates, but something seems to happen at some point that makes them invalid on subsequent requests.

From Startup.Auth.cs:

 public void ConfigureAuth(IAppBuilder app)
    {
        app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);

        app.UseCookieAuthentication(new CookieAuthenticationOptions() { CookieSecure = CookieSecureOption.Never, CookieName = "AppCookie", ExpireTimeSpan = TimeSpan.FromDays(7) });

        app.UseOpenIdConnectAuthentication(
            new OpenIdConnectAuthenticationOptions
            {
                ClientId = clientId,
                Authority = authority,
                PostLogoutRedirectUri = postLogoutRedirectUri,
                ResponseType = OpenIdConnectResponseType.CodeIdToken,
                TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
                {
                    ValidateIssuer = false,
                    RoleClaimType = "roles",
                    NameClaimType = "upn"
                },
                UseTokenLifetime = true,
                RedirectUri = postLogoutRedirectUri,
                Scope = "openid profile offline_access " + graphScopes,

                Notifications = new OpenIdConnectAuthenticationNotifications()
                {
                    RedirectToIdentityProvider = (context) =>
                    {

                        string appBaseUrl = context.Request.Scheme + "://" + context.Request.Host + context.Request.PathBase;
                        context.ProtocolMessage.RedirectUri = appBaseUrl;
                        context.ProtocolMessage.PostLogoutRedirectUri = appBaseUrl;
                        return Task.FromResult(0);
                    },

                    AuthorizationCodeReceived = OnAuthorization,
                    AuthenticationFailed = OnAuthenticationFailed
                }
            });
    }

    private Task OnAuthenticationFailed(AuthenticationFailedNotification<OpenIdConnectMessage, OpenIdConnectAuthenticationOptions> context)
    {
        context.OwinContext.Response.Redirect("/Home/Error?message=" + context.Exception.Message);
        return Task.FromResult(0);
    }

    private async Task OnAuthorization(AuthorizationCodeReceivedNotification context)
    {
        var code = context.Code;
        string signedInUserID = context.AuthenticationTicket.Identity.FindFirst(ClaimTypes.NameIdentifier).Value;

        SessionTokenStore tokenStore = new SessionTokenStore(signedInUserID, context.OwinContext.Environment["System.Web.HttpContextBase"] as HttpContextBase);
        ConfidentialClientApplication cca = new ConfidentialClientApplication(clientId, postLogoutRedirectUri, new ClientCredential(appKey), tokenStore.GetMsalCacheInstance(), null);

        var accounts = await cca.GetAccountsAsync();

        try
        {
            AuthenticationResult result = await cca.AcquireTokenByAuthorizationCodeAsync(code, graphScopes.Split(' '));
        }
        catch (MsalException ex)
        {
            string message = "AcquireTokenByAuthorizationCodeAsync threw an exception";
            context.HandleResponse();
            context.Response.Redirect($"/Home/Error?message={message}&debug={ex.Message}");
        }
        catch (Microsoft.Graph.ServiceException ex)
        {
            string message = "GetUserDetailsAsync threw an exception";
            context.HandleResponse();
            context.Response.Redirect($"/Home/Error?message={message}&debug={ex.Message}");
        }
        catch (Exception ex)
        {

            throw ex;
        }
    }

Getting the Graph Client:

 private static GraphServiceClient GetAuthenticatedClient()
    {
        try
        {
            return new GraphServiceClient(
        new DelegateAuthenticationProvider(
            async (requestMessage) =>
            {
                string signedInUserID = ClaimsPrincipal.Current.FindFirst(ClaimTypes.NameIdentifier).Value;
                SessionTokenStore tokenStore = new SessionTokenStore(signedInUserID, new HttpContextWrapper(HttpContext.Current));

                ConfidentialClientApplication cca = new ConfidentialClientApplication(appId, redirectUri.ToString(), new ClientCredential(appSecret), tokenStore.GetMsalCacheInstance(), null);

                var accounts = await cca.GetAccountsAsync();

                try
                {
                    var result = await cca.AcquireTokenSilentAsync(graphScopes.Split(' '), accounts.FirstOrDefault(), null, true);
                    requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken);
                }
                catch (MsalUiRequiredException ex)
                {

                    if (ex.ErrorCode == MsalUiRequiredException.UserNullError)
                    {
                        new HttpContextWrapper(HttpContext.Current).GetOwinContext().Authentication.Challenge(
                            new AuthenticationProperties { RedirectUri = redirectUri }, OpenIdConnectAuthenticationDefaults.AuthenticationType);

                        var account = await cca.GetAccountAsync(signedInUserID);

                        var result = await cca.AcquireTokenSilentAsync(graphScopes.Split(' '), account, null, true);
                        requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken);
                    }
                    else
                    {
                        throw ex;
                    }
                }
            }));
        }
        catch (Exception e)
        {

            throw e;
        }
    }
Matthew
  • 97
  • 1
  • 11
  • Make sure you have the latest version of ADAL, and [check the issues posted on Github](https://www.google.com/search?q=AcquireTokenSilentAsync+fails+Github). – Robert Harvey Mar 07 '19 at 17:13
  • Did you start from a sample? SessionStore is linked to the session, which might expire after 45 mins? See this SO question: https://stackoverflow.com/questions/648992/session-timeout-in-asp-net – Jean-Marc Prieur Mar 09 '19 at 02:27
  • "I've tried storing the token in Session and in Cache, and they both work when the user first authenticates, but something seems to happen at some point that makes them invalid on subsequent requests" - Could you share the error or exception message that you get with Session Cache? I have a similar ASP .NET app that works with session store provided the session timeout is long enough. – ThePretendProgrammer Mar 19 '19 at 15:04
  • Instead of storing the token in Session or Cache, I ended up storing it in the database which seems to have solved the problem, although I did also increase the session timeout. – Matthew Mar 19 '19 at 22:32

0 Answers0