ASP.NET Core Web API + Swagger + Azure B2C

Bernhard S 126 Reputation points
2024-10-19T09:44:49.0766667+00:00

Hello experts,

since weeks (with on and off phases) I try to protect my ASP.Net project with the Azure B2C. For testing if that works I want to use Swagger. But I am too stupid to make it a success. I got all kinds of error messages but I am unable to get a valid token to test the authorization. I require code help because I am blind for my mistakes. My frustration level is already high. But let me show you my status:

My project structure

User's image

But I only use the Program.cs and the appsettings.json currently.

Used for TENANT_NAME

User's image

Used for CLIENT_ID:

User's image

Used as CLIENT_SECRET:

User's image

Used for B2C_1_AdAZURE_B2C_TENANT_NAME:

User's image

The SCOPE I tried before:

User's image

But this caused: "Response status code does not indicate success: 400 (Bad Request)". So I switched it to "https://TENANT_NAME.onmicrosoft.com/CLIENT_ID/.default".

Settings of AUTHENTICATION:

User's image

My appsettings.json:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "AzureTableStorage": ""
  },
  "AzureAdB2C": {
    "Instance": "AZURE_B2C_TENANT_NAME.b2clogin.com",
    "Domain": "AZURE_B2C_TENANT_NAME.onmicrosoft.com",
    "ClientId": "CLIENT_ID",
    "ClientSecret": "CLIENT_SECRET",
    "Tenant": "AZURE_B2C_TENANT_NAME",
    "SignUpSignInPolicyId": "B2C_1_AdAZURE_B2C_TENANT_NAME",
    "Scope": "https://AZURE_B2C_TENANT_NAME.onmicrosoft.com/CLIENT_ID/.default",     "AuthorizationUrl": "https://AZURE_B2C_TENANT_NAME.b2clogin.com/AZURE_B2C_TENANT_NAME.onmicrosoft.com/oauth2/v2.0/authorize?p=B2C_1_AdAZURE_B2C_TENANT_NAME",
    "TokenUrl": "https://AZURE_B2C_TENANT_NAME.b2clogin.com/AZURE_B2C_TENANT_NAME.onmicrosoft.com/oauth2/v2.0/token?p=B2C_1_AdAZURE_B2C_TENANT_NAME",
    "RedirectUri": "https://localhost:7169/swagger/oauth2-redirect.html"
  },
  "Cors": {
    "AllowedOrigins": [ "https://localhost:7169" ]
  }
}

The Program.cs:

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.Identity.Web;
using Microsoft.OpenApi.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Identity.Client;
using System.Net.Http.Headers;
using Swashbuckle.AspNetCore.Annotations;
var builder = WebApplication.CreateBuilder(args);
string tenantName = builder.Configuration["AzureAdB2C:Tenant"];
string instance = builder.Configuration["AzureAdB2C:Instance"];
string domain = builder.Configuration["AzureAdB2C:Domain"];
string clientId = builder.Configuration["AzureAdB2C:ClientId"];
string clientSecret = builder.Configuration["AzureAdB2C:ClientSecret"];
string authority = $"https://{instance}/tfp/{domain}/{builder.Configuration["AzureAdB2C:SignUpSignInPolicyId"]}/v2.0/";
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddMicrosoftIdentityWebApi(options =>
    {
        options.Authority = authority;
        options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidIssuer = $"https://{instance}/{domain}/v2.0/",
            ValidateAudience = true,
            ValidAudience = clientId,
            ValidateLifetime = true
        };
        options.Events = new JwtBearerEvents
        {
            OnAuthenticationFailed = context =>
            {
                Console.WriteLine($"Authentication failed: {context.Exception}");
                return Task.CompletedTask;
            }
        };
    }, options => { builder.Configuration.Bind("AzureAdB2C", options); });
builder.Services.AddAuthorization();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(c =>
{
    c.SwaggerDoc("v1", new OpenApiInfo { Title = "My API", Version = "v1" });
    c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
    {
        Type = SecuritySchemeType.Http,
        Scheme = "bearer",
        BearerFormat = "JWT",
        In = ParameterLocation.Header,
        Name = "Authorization",
        Description = "JWT Authorization header using the Bearer scheme."
    });
    c.AddSecurityRequirement(new OpenApiSecurityRequirement
    {
        {
            new OpenApiSecurityScheme
            {
                Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "Bearer" }
            },
            Array.Empty<string>()
        }
    });
});
builder.Services.AddCors(options =>
{
    options.AddDefaultPolicy(builder =>
    {
        builder.AllowAnyOrigin()
               .AllowAnyMethod()
               .AllowAnyHeader();
    });
});
var app = builder.Build();
async Task<string> GetAccessTokenAsync()
{
    var app = ConfidentialClientApplicationBuilder
        .Create(clientId)
        .WithAuthority(authority)
        .WithClientSecret(clientSecret)
        .Build();
    try
    {
        var result = await app.AcquireTokenForClient(new[] { builder.Configuration["AzureAdB2C:Scope"] }).ExecuteAsync();
        return result.AccessToken;
    }
    catch (MsalServiceException ex)
    {
        Console.WriteLine($"MSAL Service Exception: {ex.Message}");
        return null;
    }
    catch (Exception ex)
    {
        Console.WriteLine($"Unexpected error: {ex.Message}");
        return null;
    }
}
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI(c =>
    {
        c.SwaggerEndpoint("/swagger/v1/swagger.json", "v1");
        c.OAuthClientId(clientId);
        c.OAuthAppName(tenantName);
        c.OAuthUsePkce();
        c.OAuthScopes(builder.Configuration["AzureAdB2C:Scope"]);
        c.RoutePrefix = string.Empty;
    });
}
app.UseCors();
app.UseAuthentication();
app.UseAuthorization();
app.MapGet("/", () => "Hello World!")
   .WithName("GetHelloWorld")
   .WithMetadata(new SwaggerOperationAttribute("Get Hello World", "Returns a simple hello world message"));
app.MapGet("/test", [Authorize] () => "Authorization successful!")
   .WithName("GetTest")
   .WithMetadata(new SwaggerOperationAttribute("Test Endpoint", "Returns a success message if authorized"));
app.MapGet("/testtoken", async (HttpContext httpContext) =>
{
    var token = await GetAccessTokenAsync();
    if (token == null)
    {
        return Results.Problem("Failed to acquire token");
    }
    using var client = new HttpClient();
    client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
    var response = await client.GetAsync("https://localhost:7169/test");  // Replace with your actual URL
    if (response.IsSuccessStatusCode)
    {
        return Results.Ok("Token acquisition and API call successful");
    }
    return Results.Problem($"API call failed: {response.StatusCode}");
})
.WithName("GetTestToken")
.WithMetadata(new SwaggerOperationAttribute("Test Token Acquisition", "Acquires a token and calls the test endpoint"));
app.MapGet("/claims", (HttpContext httpContext) =>
{
    return Results.Ok(httpContext.User.Claims.Select(c => new { c.Type, c.Value }));
})
.WithName("GetClaims")
.WithMetadata(new SwaggerOperationAttribute("Claims Endpoint", "Returns the claims of the authenticated user"));
app.Run();

FAILS/RESULTS:

1.) Try

Create a Test Method to get a token

User's image

User's image

Copy the token

User's image

User's image

Authenticate in Swagger

User's image

The dialog closes. Testing "/test":

User's image

Server response
Code	Details
401
Undocumented
Error: response status is 401

Response headers
 content-length: 0 
 date: Sat,19 Oct 2024 09:30:09 GMT 
 server: Kestrel 
 www-authenticate: Bearer error="invalid_token" 

  1. Try

User's imageUser's image

{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.6.1",
  "title": "An error occurred while processing your request.",
  "status": 500,
  "detail": "API call failed: Unauthorized"
}

Any ideas?

My code is a mess and I don't need to keep it. All I want to achive is to make a authorized GET request as a test to verify that my Api endpoint is secured by the Bearer token.

(I have anonymized confidential information. If I forget something, please let me know.)

Developer technologies ASP.NET ASP.NET Core
Developer technologies ASP.NET ASP.NET API
Microsoft Security Microsoft Entra Microsoft Entra External ID
Microsoft Security Microsoft Entra Microsoft Entra ID
{count} votes

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.