Blazor App Redirecting API response to login page

Amjad Khan 41 Reputation points
2024-10-05T17:18:09.84+00:00

I am building a Blazor Server app on .NET. The app was started using the Visual Studio .NET8 Web template for Blazor server with individual authentication turned on. In the second step, I added an API controller to the app, with the [Authorize] attribute. Here is my program.cs.

public class Program
{
    public static void Main(string[] args)
    {
        var builder = WebApplication.CreateBuilder(args);
        builder.Services.AddControllers(); //added for API
        // Add services to the container.
        builder.Services.AddRazorComponents()
            .AddInteractiveServerComponents();
        builder.Services.AddCascadingAuthenticationState();
        builder.Services.AddScoped<IdentityUserAccessor>();
        builder.Services.AddScoped<IdentityRedirectManager>();
        builder.Services.AddScoped<AuthenticationStateProvider, IdentityRevalidatingAuthenticationStateProvider>();
        builder.Services.AddAuthentication(options =>
            {
                options.DefaultScheme = IdentityConstants.ApplicationScheme;
                options.DefaultSignInScheme = IdentityConstants.ExternalScheme;
                
            })
            .AddBearerToken(IdentityConstants.BearerScheme)
            .AddIdentityCookies();
        
        var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found.");
        //builder.Services.AddDbContextFactory<ApplicationDbContext>(options =>
        //    options.UseSqlServer(connectionString));
        builder.Services.AddDbContextFactory<ApplicationDbContext>(options =>
options.UseMySql(connectionString, new MySqlServerVersion(new Version(8, 0, 26)),
                                            mySqlOptions =>
                                            {
                                                mySqlOptions
                                                .EnableRetryOnFailure(
                                                maxRetryCount: 10,
                                                maxRetryDelay: TimeSpan.FromSeconds(30),
                                                errorNumbersToAdd: null);
                                            }
                                         )
.EnableSensitiveDataLogging(true));//should be scoped as ApplicationDbContext uses the TenantDbContext which is also scoped. By default the service is Singleton
        builder.Services.AddDbContext<ApplicationDbContext>(options =>
            options.UseSqlServer(connectionString));
        builder.Services.AddDatabaseDeveloperPageExceptionFilter();
        builder.Services.AddIdentityCore<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = true)
            .AddEntityFrameworkStores<ApplicationDbContext>()
            .AddSignInManager()
            .AddDefaultTokenProviders();
        builder.Services.AddSingleton<IEmailSender<ApplicationUser>, EmailSender>();
        builder.Services.AddSingleton<RegistrationDataSaver>();
        builder.Services.AddAuthorization(); //added for API
        builder.Services.AddEndpointsApiExplorer(); //Added for API
        builder.Services.AddSwaggerGen(); //Added for API
        builder.Services.AddCors(options =>
        {
            options.AddPolicy("AllowAllOrigins",
                builder =>
                {
                    builder.AllowAnyOrigin()
                           .AllowAnyMethod()
                           .AllowAnyHeader();
                });
        });
        // Inside the ConfigureServices method
        builder.Services.AddHttpClient();
        builder.Services.AddMudServices();
        var app = builder.Build();
        app.MapControllers(); //added for API
        app.MyMapIdentityApi<ApplicationUser>();
        // Configure the HTTP request pipeline.
        if (app.Environment.IsDevelopment())
        {
            app.UseMigrationsEndPoint();
            app.UseSwagger(); //added for API
            app.UseSwaggerUI(); //added for API
        }
        else
        {
            app.UseExceptionHandler("/Error");
            // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
            app.UseHsts();
        }
        app.UseHttpsRedirection();
        app.UseStaticFiles();
        app.UseAntiforgery();
        app.MapRazorComponents<App>()
            .AddInteractiveServerRenderMode();
        // Add additional endpoints required by the Identity /Account Razor components.
        app.MapAdditionalIdentityEndpoints();
        app.UseAuthorization();//added for API
        app.UseCors(policy =>
        policy.WithOrigins("http://localhost:7217", "https://localhost:7217")
            .AllowAnyMethod()
            .WithHeaders(HeaderNames.ContentType, HeaderNames.Authorization,
                "x-custom-header")
            .AllowCredentials()
             );
        app.Run();
    }
}

I then use the login endpoint that was part of the Blazor server project to login and obtain a bearer token in return:

private async Task OnValidSubmit()
{
    //login to license server and get token
    var response = await http.PostAsJsonAsync("https://localhost:44325/login", model); //this uses the injected client

    if (!response.IsSuccessStatusCode)
    {
        Snackbar.Add("License could not be saved.", Severity.Error);

    }
    AccessTokenResponse tokens;
    tokens = response.Content.ReadFromJsonAsync<AccessTokenResponse>().Result;

    //create a new client because we will be changing its auth header to connect to license server API.
    var httpClient = new HttpClient();
    
    httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokens.AccessToken.ToString());
    
    //call API with token set
    HttpResponseMessage respon = await httpClient.GetAsync($"https://localhost:44325/api/License/GetLicense/{email}/{product_name}");
    if (respon.IsSuccessStatusCode)
    {
        var jsonResponse = await respon.Content.ReadAsStringAsync(); //here I receive the Login page instead of expected response which is my License object.
        var license = JsonSerializer.Deserialize<License>(jsonResponse);
    }
}

The question is, why would I be redirected to the Login page when calling the API? I have confirmed that I am receiving the bearer token from the login request. But after I set the authorization header in the next http request and send to the API, instead of receiving my expected data, I am receiving the Blazor login page.

Blazor
Blazor
A free and open-source web framework that enables developers to create web apps using C# and HTML being developed by Microsoft.
1,596 questions
{count} votes

1 answer

Sort by: Most helpful
  1. Bruce (SqlWork.com) 66,706 Reputation points
    2024-10-06T00:10:30.0033333+00:00

    Blazor server authentication is cookie based. If you use oauth instead of individual, the server code still uses cookie, but you can get the access token to make an api call to another webserver requiring a token.

    as Blazor server is a single http request per app load, authentication is done before the app load and passed via the HttpContext to the blazor app. The Blazor app it self use the authentication state, built from httpcontext.


Your answer

Answers can be marked as Accepted Answers by the question author, which helps users to know the answer solved the author's problem.