search

Home  >  Q&A  >  body text

How to configure Azure AD authentication in hybrid ASP.NET Core MVC (backend) and Vuejs SPA (frontend)?

My application is a hybrid approach, using ASP.NET Core MVC as the backend. I have various controllers that my frontend uses to pull data from the database and make API calls on MS Graph. I use the following program.cs file to initiate authentication when a user logs into the website for the first time:

//authentication pipline
builder.Services.AddHttpContextAccessor();
var initialScopes = builder.Configuration.GetValue<string>("DownstreamApi:Scopes")?.Split(' ');
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
                .AddMicrosoftIdentityWebApp(options =>
                {
                    builder.Configuration.Bind("AzureAd", options);
                    options.Events = new OpenIdConnectEvents
                    {
                        //Tap into this event to add a UserID Claim to a new HttpContext identity
                        OnTokenValidated = context =>
                        {
                            //This query returns the UserID from the DB by sending the email address in the claim from Azure AD
                            string query = "select dbo.A2F_0013_ReturnUserIDForEmail(@Email) as UserID";
                            string connectionString = builder.Configuration.GetValue<string>("ConnectionStrings:DBContext");
                            string signInEmailAddress = context.Principal.FindFirst   Value("preferred_username");

                            using (var connection = new SqlConnection(connectionString))
                            {
                                var queryResult = connection.QueryFirst(query, new { Email = signInEmailAddress });

                                var claims = new List<Claim>
                                {
                                    new Claim("UserID", queryResult.UserID.ToString())
                                };

                                var appIdentity = new ClaimsIdentity(claims);

                                context.Principal.AddIdentity(appIdentity);
                            }

                            return Task.CompletedTask;
                        },
                    };
                }).EnableTokenAcquisitionToCallDownstreamApi(initialScopes)
                        .AddMicrosoftGraph(builder.Configuration.GetSection("DownstreamApi"))
                        .AddInMemoryTokenCaches();

//Add Transient Services
builder.Services.AddTransient<IOneDrive, OneDrive>();

builder.Services.AddControllers(options =>
{
    var policy = new AuthorizationPolicyBuilder()
        .RequireAuthenticatedUser()
        .Build();
    options.Filters.Add(new AuthorizeFilter(policy));
}).AddMicrosoftIdentityUI();

builder.Services.AddRazorPages().AddRazorPagesOptions(options =>
{
    options.Conventions.AllowAnonymousToFolder("/Login");
    options.Conventions.AuthorizeFolder("/");
    options.Conventions.AuthorizeFolder("/files");
}).AddMicrosoftIdentityUI();

// Add the UI support to handle claims challenges
builder.Services.AddServerSideBlazor()
   .AddMicrosoftIdentityConsentHandler();
builder.Services.AddRequiredScopeAuthorization();

In the Azure AD portal, my application is registered as a web application. Therefore, when users initially visit the website, they are redirected to https://login.microsoftonline.com/blahblah to begin the login process. This is performed automatically by the Azure AD identity platform. Then, once the login occurs, they are redirected to the localhost (localhost:43862) where VueJS spa is loaded. My spa uses various axios requests to controllers, which pull data, and vue router to load components. However, my problem is that the user needs to log back in because the cookie has expired or they logged out in another tab. The next axios request from an expired session does not redirect the user to the Azure login screen but results in a CORS error. So I need to have my axios request force the page to redirect to the Azure AD login screen (which is probably the worst idea since the CORS policy will cause errors), or have it redirect back to localhost/login, which is what I Your own custom login screen using the Azure AD login button should not affect CORS. So how do I intercept this Azure AD redirect to the Azure AD login and replace it with my own?

I also tried returning a 401 error code so I could check for that in the axios request, but to no avail, it does nothing. If I put a breakpoint there, it does hit this code, but it doesn't change the status code of the response, I still get a 302. My code is trying to add to the event:

OnRedirectToIdentityProvider = context =>
                    {
                        context.Response.StatusCode = 401;
                        return Task.CompletedTask;
                    }

My other thoughts are maybe I should set up the CORS policy to allow redirects from login.microsoft.com? Or is this bad practice?

P粉242741921P粉242741921339 days ago610

reply all(1)I'll reply

  • P粉501007768

    P粉5010077682024-01-09 09:14:02

    I can answer part of your question... First, for our Azure AD protected API application, what the API should do is verify that the request contains the correct access token in the request header, and if so, give output a response, if not, an error such as 401 or 403 is given. A normal API application should not have a UI to log users in. Anyway, if you want to expose an API in an MVC project, that's OK, but for the API itself, it shouldn't have a UI.

    Let's look at the example below, I have a .net 6 Web api project, this is my program.cs:

    using Microsoft.Identity.Web;
    
    var builder = WebApplication.CreateBuilder(args);
    // Add services to the container.
    builder.Services.AddMicrosoftIdentityWebApiAuthentication(builder.Configuration);
    builder.Services.AddControllers();
    // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
    builder.Services.AddEndpointsApiExplorer();
    builder.Services.AddSwaggerGen();
    
    var app = builder.Build();
    
    // Configure the HTTP request pipeline.
    if (app.Environment.IsDevelopment())
    {
        app.UseSwagger();
        app.UseSwaggerUI();
    }
    
    app.UseHttpsRedirection();
    app.UseAuthentication();
    app.UseAuthorization();
    app.MapControllers();
    app.Run();

    And needs to be configured in appsetting.json.

    "AzureAd": {
        "Instance": "https://login.microsoftonline.com/",
        "ClientId": "azure_ad_client_id",
        "ClientSecret": "client_secret",
        "Domain": "tenant_id",
        "TenantId": "tenant_id",
        //"Audience": "api://azure_ad_client_id_which exposed_api" // here I used the same azure ad app to expose API, so I can comment this property
      },

    This is the controller:

    [ApiController]
        [Route("[controller]")]
        [Authorize]
        public class WeatherForecastController : ControllerBase
        {
            [RequiredScope("Tiny.Read")]
            [HttpGet]
            public string Get()
            {
                return "world";
            }
        }

    I have an Azure AD application and expose the following API:

    I also added this API for the same Azure AD app.

    Then let’s do a test. When I call this API directly, I get a 401 error:

    I also get a 401 error if I use an expired token in the request:

    But if I use the correct token (go to https://jwt.io to decode the token, we should see that it contains the correct scope, and to me it "scp": "Tiny.Read",), I will get the response:

    At this point, the API part has been completed. Let's look at client SPA. For SPA, you should integrate MSAL so that you can have your users log in via Azure AD and generate access tokens for calling the MS graph API or your own API. The code to generate the access token should be the same, but you should set different scopes for different APIs. In my scenario, my API requires a scope Tiny.Read, then I should set it in my client application.

    This is a screenshot of generating access token in react. You need to set the scope in your code.

    Now that you have a way to generate an access token, you already know the API url. Then you can send a request to call the api, use AJAX, use fetch, or whatever, just send an http request. And in the part of calling the api, the response also needs to be processed. If the response code is 401, then you need to perform some logic, possibly redirecting to the login page. You said you're having trouble here, that you're having a CORS issue. I can't answer this part. I think it depends on how you redirect to the Azure AD login page. I'm afraid you can take a look at this example to see how to log in a user and call the graph api.

    reply
    0
  • Cancelreply