'Adding an authenticated API to an existing C# MVC.Net (Framework) App that uses Azure SSO

Perhaps my google fu is not strong enough to find the answer but here's the question.

My organization is in the process of creating a SSO Enabled MVC.Net (Framework) application. We currently have a working application that can log users in using the Azure AD and store the user token in a SQL database table. This is great when doing anything from the server based controller actions, but I would like to add an authenticated API into the mix.

How can I ensure the usertoken (currently stored in SQL instead of session) can be securely passed to the client, then back to the API where it can be used / refreshed when needed?

On StartupAuth.Cs

    private async Task OnAuthorizationCodeReceivedAsync(AuthorizationCodeReceivedNotification notification)
    {
        notification.HandleCodeRedemption();

        var idClient = ConfidentialClientApplicationBuilder.Create(appId)
            .WithAuthority(authority)
            .WithRedirectUri(redirectUri)
            .WithClientSecret(appSecret)
            //.WithCertificate(certificate) //Look into this change for when it's live
            .Build();

        var signedInUser = new ClaimsPrincipal(notification.AuthenticationTicket.Identity);
        //var tokenStore = new SessionTokenStore(idClient.UserTokenCache, HttpContext.Current, signedInUser);
        var tokenStore = new SQLTokenStore(idClient.UserTokenCache, signedInUser);

        try
        {
            string[] scopes = graphScopes.Split(' ');

            var result = await idClient.AcquireTokenByAuthorizationCode(
                scopes, notification.Code).ExecuteAsync();

            var userDetails = await GraphHelper.GetUserDetailsAsync(result.AccessToken);

            tokenStore.SaveUserDetails(userDetails);
            notification.HandleCodeRedemption(null, result.IdToken);
        }
        catch (MsalException ex)
        {
            string message = "AcquireTokenByAuthorizationCodeAsync threw an exception";
            notification.HandleResponse();
            notification.Response.Redirect($"{ConfigurationManager.AppSettings["iis:subsiteURL"]}Home/Error?message={message}&debug={ex.Message}");
        }
        catch (Microsoft.Graph.ServiceException ex)
        {
            string message = "GetUserDetailsAsync threw an exception";
            notification.HandleResponse();
            notification.Response.Redirect($"{ConfigurationManager.AppSettings["iis:subsiteURL"]}Home/Error?message={message}&debug={ex.Message}");
        }
    }

We then store use the SQLTokenStore class to store and retrieve the user token. In the abstract class BaseController we retrieve the Token and pass user information to the front end.

    public abstract class BaseController : Controller
    {
        public CustViewModels custViewModels = new CustViewModels();
        public string Query { get; set; }
        public CachedUser userInfo { get; set; }

        protected void Flash(string message, string debug = null)
        {
            var alerts = TempData.ContainsKey(Alert.AlertKey) ?
                (List<Alert>)TempData[Alert.AlertKey] :
                new List<Alert>();

            alerts.Add(new Alert
            {
                Message = message,
                Debug = debug
            });

            TempData[Alert.AlertKey] = alerts;
        }

        protected override async void OnActionExecuting(ActionExecutingContext filterContext)
        {
            Query = System.Web.HttpContext.Current.Request.QueryString.Get("PReq");
            if (string.IsNullOrEmpty(Query))
            {
                Query = string.Empty;
            }
            if (Request.IsAuthenticated)
            {
                // Get the user's token cache
                //var tokenStore = new SessionTokenStore(null, System.Web.HttpContext.Current, ClaimsPrincipal.Current);
                var tokenStore = new SQLTokenStore(null, ClaimsPrincipal.Current);

                if (tokenStore.HasData())
                {
                    // Add the user to the view bag
                    userInfo = tokenStore.GetUserDetails();
                    custViewModels.currentUser = userInfo;
                    ViewBag.User = userInfo;
#if DEBUG
                    ViewBag.isDebug = true;
                    ViewBag.TokenStore = tokenStore.GetUserToken();                    
#endif
                    if (!string.IsNullOrEmpty(userInfo.UserPrincipalName))
                    {
                        if (!Regex.IsMatch(userInfo.UserPrincipalName, @"^[a-zA-Z0-9_.+-]+@(?:(?:[a-zA-Z0-9-]+\.)?[a-zA-Z]+\.)?(someorg)\.com$"))
                        {
                            string ErrorMessage = $"The email account {userInfo.Email} is not authorized to use this application. Please sign in using your someorg email account.";
                            filterContext.Result = RedirectToAction("Error", "Unauthorized", new { message = ErrorMessage, debug = "" });
                        }
                    }
                }
                else
                {
                    // The session has lost data. This happens often
                    // when debugging. Log out so the user can log back in
                    Request.GetOwinContext().Authentication.SignOut(CookieAuthenticationDefaults.AuthenticationType);
                    filterContext.Result = RedirectToAction("Index", "Home");
                }
            }
            else if (!string.IsNullOrEmpty(Query))
            {
                filterContext.Result = RedirectToAction("SignIn", "Account", new { qString = Query });
            }
            else
            {
                //filterContext.Result = RedirectToAction("Index", "Menu");
            }

            base.OnActionExecuting(filterContext);
        }
    }
  1. How would I pass that auth token to the end user so that I could then use an API Controller to consume a Microsoft Graph function that would be called by JavaScript on a timer basis to refresh on the page.

  2. Since the user requesting the api call should still be the same logged in user do I even need to authenticate? Would I not just use the following function inside my GraphHelper class to get the currently authenticated user again?

     private static GraphServiceClient GetAuthenticatedClient()
     {
         return new GraphServiceClient(
             new DelegateAuthenticationProvider(
                 async (requestMessage) =>
                 {
                     var idClient = ConfidentialClientApplicationBuilder.Create(appId)
                         .WithRedirectUri(redirectUri)
                         .WithClientSecret(appSecret)
                         .Build();
    
                     //var tokenStore = new SessionTokenStore(idClient.UserTokenCache, HttpContext.Current, ClaimsPrincipal.Current);
                     var tokenStore = new SQLTokenStore(idClient.UserTokenCache, ClaimsPrincipal.Current);
    
                     var accounts = await idClient.GetAccountsAsync();
    
                     // By calling this here, the token can be refreshed
                     // if it's expired right before the Graph call is made
                     var result = await idClient.AcquireTokenSilent(graphScopes, accounts.FirstOrDefault())
                                 .ExecuteAsync();
    
                     requestMessage.Headers.Authorization =
                         new AuthenticationHeaderValue("Bearer", result.AccessToken);
                 }));
     }
    

Any advice would be great, I'm still very new to using SSO in my own applications.



Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source