Search code examples
authenticationswaggerasp.net-core-webapiswashbuckle

ASP.NET bearer authentication, not working with swagger. Is working with postman


I am trying to setup authentication for my ASP.NET web api. I have added jwt authentication using Firebase Google as my identity provider. When i am trying to authenticate through swagger ui, i can generate my jwt token, i can add the token ty the "authorize" button in swagger ui, but when i try to call an endpoint requiring auth i get 401. When i do the exact same request from postman i get 200, i just dont understand why!

My setup in program.cs:

Swagger gen setup:

...
builder.Services.AddSwaggerGen(options =>
{
    options.SwaggerDoc("v1", new OpenApiInfo { Title = "WorkoutBuddy", Version = "v1" });
    options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
    {
        In = ParameterLocation.Header,
        Name = "Authorization",
        Description = "Provide JWT token",
        Type = SecuritySchemeType.Http,
        BearerFormat = "JWT",
        Scheme = "bearer"
    });
    options.AddSecurityRequirement(new OpenApiSecurityRequirement{
        {
            new OpenApiSecurityScheme
            {
                Reference = new OpenApiReference
                {
                    Type=ReferenceType.SecurityScheme,
                    Id="Bearer"
                }
            },
            Array.Empty<string>()
        }
    });
});
...

JWT setup:

...
builder.Services
    .AddAuthentication()
    .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, jwtOptions =>
    {
        jwtOptions.Authority = builder.Configuration["Auth:ValidIssuer"];
        jwtOptions.Audience = builder.Configuration["Auth:Audience"];
        jwtOptions.TokenValidationParameters.ValidIssuer = builder.Configuration["Auth:ValidIssuer"];
    });
...

Swagger UI setup

...
    app.UseExceptionHandler("/error")
        .UseSwagger()
        .UseSwaggerUI(c =>
        {
            c.SwaggerEndpoint($"/swagger/v1/swagger.json", "WorkoutBuddy Api");
        });

    app.UseCors(options => options.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod());

    app.UseHttpsRedirection();

    app.UseAuthentication();
    app.UseAuthorization();

    app.MapControllers();

    app.Run();
...

Swagger UI request - 401

Postman request - 200

Tried configuring swagger ui all possible ways i could find. Nothing seems to work

Update

Here is my full Program.cs

global using WorkoutBuddy.Data;
using Microsoft.EntityFrameworkCore;
using Azure.Identity;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using FirebaseAdmin;
using Google.Apis.Auth.OAuth2;
using WorkoutBuddy.Services;
using WorkoutBuddy.Features;
using Microsoft.OpenApi.Models;
using Microsoft.IdentityModel.Tokens;
using Swashbuckle.AspNetCore.Filters;

var builder = WebApplication.CreateBuilder(args);

var env = builder.Environment;

builder.Configuration
       .SetBasePath(env.ContentRootPath)
       .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
       .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
       .AddEnvironmentVariables()
       .AddUserSecrets<Program>();

// var azureCredentials = new DefaultAzureCredential(new DefaultAzureCredentialOptions
// {
//     ExcludeEnvironmentCredential = false,
//     ExcludeManagedIdentityCredential = false,
//     ExcludeSharedTokenCacheCredential = false,
//     ExcludeVisualStudioCredential = true,
//     ExcludeVisualStudioCodeCredential = true,
//     ExcludeAzureCliCredential = false,
//     ExcludeAzurePowerShellCredential = true,
//     ExcludeInteractiveBrowserCredential = true,
// });

// // Inject keyvault secrets
// var shouldUseKeyVault = builder.Configuration.GetValue<bool>("KeyVault:UseKeyVault", true);
// if (shouldUseKeyVault)
// {
//     var keyVaultUrl = builder.Configuration.GetValue<Uri>("KeyVault:Url"); // ?? throw new ArgumentNullException("Missing configuration: KeyVault:Url");

//     builder.Configuration.AddAzureKeyVault(keyVaultUrl, azureCredentials);
// }

// Setup EF Core to SQL db connection
builder.Services.AddDbContext<DataContext>(options =>
{
    var sqlDbConnectionString = builder.Configuration.GetConnectionString("SQL") ?? throw new ArgumentNullException("Missing SQL connectionstring");
    options.UseSqlServer(sqlDbConnectionString, sqlServerOptions =>
    {
        sqlServerOptions.CommandTimeout(30);
    });
});

// setup swagger options
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
// https://www.youtube.com/watch?v=z46lqVOv1hQ&ab_channel=ThumbIKR-ProgrammingExamples
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(options =>
{
    options.SwaggerDoc("v1", new OpenApiInfo { Title = "WorkoutBuddy", Version = "v1" });
    options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
    {
        In = ParameterLocation.Header,
        Name = "Authorization",
        Description = "Provide JWT token",
        Type = SecuritySchemeType.Http,
        BearerFormat = "JWT",
        Scheme = "bearer"
    });
    // options.AddSecurityRequirement(new OpenApiSecurityRequirement{
    //     {
    //         new OpenApiSecurityScheme
    //         {
    //             Reference = new OpenApiReference
    //             {
    //                 Type=ReferenceType.SecurityScheme,
    //                 Id="Bearer"
    //             }
    //         },
    //         Array.Empty<string>()
    //     }
    // });
    options.OperationFilter<SecurityRequirementsOperationFilter>();
});

// setup application insight logging
// if (env.EnvironmentName != "Local")
// {
//     builder.Services.AddApplicationInsightsTelemetry(options =>
//     {
//         options.DeveloperMode = false;
//         options.ConnectionString = builder.Configuration.GetValue<string>("APPLICATIONINSIGHTS_CONNECTION_STRING") ?? throw new ArgumentNullException("Missing: APPLICATIONINSIGHTS_CONNECTION_STRING");
//     });
// }

// setup firebase authentication
// Convert firebase config json to string: https://tools.knowledgewalls.com/json-to-string
var firebaseConfig = builder.Configuration.GetValue<string>("Auth:FirebaseConfig"); // ?? throw new ArgumentNullException("missing firebase config");
FirebaseApp.Create(new AppOptions()
{
    Credential = GoogleCredential.FromJson(firebaseConfig)
});

builder.Services
    .AddAuthentication()
    .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, jwtOptions =>
    {
        jwtOptions.Authority = builder.Configuration["Auth:ValidIssuer"];
        jwtOptions.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true,
            ValidIssuer = builder.Configuration.GetValue<string>("Auth:ValidIssuer"),
            ValidAudience = builder.Configuration.GetValue<string>("Auth:Audience")
        };
        jwtOptions.Events = new JwtBearerEvents
        {
            OnAuthenticationFailed = context =>
            {
                Console.WriteLine("Unauthorized request: \n" + context.Exception);
                return Task.CompletedTask;
            }
        };
    });

// setup services
builder.Services.AddHttpContextAccessor();
// builder.Services.AddOutputCache(); // can be faulty if multiple instances

builder.Services.AddHttpClient<GoogleJwtProvider, GoogleJwtProvider>(httpClient =>
{
    var baseAddress = builder.Configuration["Auth:TokenUri"] ?? throw new ArgumentNullException("Missing: Auth:TokenUri");
    var apiKey = builder.Configuration["Auth:FirebaseApiKey"] ?? throw new ArgumentException("Missing firebase api key");
    httpClient.BaseAddress = new Uri($"{baseAddress}?key={apiKey}");
});
builder.Services.AddScoped<UserService, UserService>();
builder.Services.AddScoped<ProfileService, ProfileService>();
builder.Services.AddScoped<WorkoutDetailService, WorkoutDetailService>();
builder.Services.AddScoped<ExerciseDetailService, ExerciseDetailService>();
builder.Services.AddScoped<WorkoutService, WorkoutService>();

var app = builder.Build();

switch (args.FirstOrDefault())
{
    case "initAndSeedDb":
        await app.InitAndSeedDb();
        return;
    case null:
        RunApp(app);
        return;
    case var arg: throw new ArgumentException($"Unknown command-line argument: {arg}");
}

static void RunApp(WebApplication app)
{
    // app.UseExceptionHandler("/error")
    //     .UseSwagger()
    //     .UseSwaggerUI(c =>
    //     {
    //         c.SwaggerEndpoint($"/swagger/v1/swagger.json", "WorkoutBuddy Api");
    //     });

    app.UseSwagger()
    .UseSwaggerUI(c =>
    {
        c.SwaggerEndpoint($"/swagger/v1/swagger.json", "WorkoutBuddy Api");
    });

    app.UseCors(options => options.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod());

    app.UseHttpsRedirection();
    // app.UseOutputCache(); // can be faulty if multiple instances of app running

    app.UseAuthentication();
    app.UseAuthorization();

    app.MapControllers();

    app.Run();
}

Here is the controller im trying to call:

[Authorize]
[ApiController]
[Route("api/workout")]
public class WorkoutController : CustomControllerBase
{
    private readonly ILogger<WorkoutController> _logger;
    private readonly WorkoutService _workoutService;

    public WorkoutController(
        ILogger<WorkoutController> logger,
        WorkoutService workoutService)
    {
        _logger = logger;
        _workoutService = workoutService;
    }

    [HttpGet()]
    public async Task<ActionResult<IEnumerable<WorkoutResponse>>> GetAllWorkoutsByProfile()
    {
        var workoutsResult = await _workoutService.GetWorkoutsForProfile();

        return GetDataOrError(
            result: workoutsResult,
            resolveResponse: (w) => w.Select(e => new WorkoutResponse(e))
        );
    }

    [HttpGet("{id}")]
    public async Task<ActionResult<WorkoutResponse>> GetWorkoutById([FromRoute][Required] Guid workoutId)
    {
        var workoutResult = await _workoutService.GetWorkoutById(workoutId);

        return GetDataOrError(
            result: workoutResult,
            resolveResponse: (w) => new WorkoutResponse(w)
        );
    }

    // delete workout by id 
    [HttpDelete("{id}")]
    public async Task<ActionResult<WorkoutResponse>> DeleteWorkout([FromRoute][Required] Guid workoutId)
    {
        var workoutResult = await _workoutService.DeleteWorkout(workoutId);

        return GetDataOrError(
            result: workoutResult,
            resolveResponse: (w) => new WorkoutResponse(w)
        );
    }

    // create workoutset by workout id

    // create exerciseset by workoutset id

    // update exerciseset by workoutset id
}

I have also noticed with the current configuration i have the lockpads looks strange after i authenticate in swagger UI:

Swagger UI after authenticated


Solution

  • After creating a new project from scratch, reusing all my authentication and swagger setup form my original project i finally found a reason. When i access my swagger ui with http: http://localhost:5173/swagger/index.html my swagger fails authentication. Postman works fine when authenticating to my api with http. But when i switch to using swagger ui with https: https://localhost:5272/swagger/index.html authentication with swagger ui works as expected.