Я пытаюсь настроить аутентификацию для своего веб-API ASP.NET. Я добавил аутентификацию jwt, используя Firebase Google в качестве поставщика удостоверений. Когда я пытаюсь пройти аутентификацию через пользовательский интерфейс Swagger, я могу сгенерировать свой токен JWT, я могу добавить токен с помощью кнопки «Авторизовать» в пользовательском интерфейсе Swagger, но когда я пытаюсь вызвать конечную точку, требующую аутентификации, я получаю 401. Когда я делаю тот же запрос от почтальона, я получаю 200, я просто не понимаю, почему!
Моя настройка в program.cs:
Настройка Swagger Gen:
...
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:
...
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
...
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 всеми возможными способами, которые смог найти. Кажется, ничего не работает
Вот мой полный файл 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://thewikihow.com/video_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();
}
Вот контроллер, который я пытаюсь вызвать:
[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
}
Я также заметил, что в текущей конфигурации блокировка выглядит странно после аутентификации в пользовательском интерфейсе Swagger:
Я изменил параметры в вашем методе AddJwtBearer
, он работает в моем локальном методе.
Вот пример кода
Программа.cs
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi.Models;
using System.Text;
namespace WebApplication1
{
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
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>()
}
});
});
builder.Services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateIssuerSigningKey = true,
ValidIssuer = "Auth:ValidIssuer",
ValidAudience = "Auth:Audience",
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("this is my custom Secret key for authentication"))
};
});
var app = builder.Build();
// Configure the HTTP request pipeline.
app.UseSwagger();
app.UseSwaggerUI();
app.UseHttpsRedirection();
app.UseCors(options => options.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod());
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
}
}
}
Мой тестовый контроллер
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
namespace WebApplication1.Controllers
{
[Authorize]
[ApiController]
[Route("[controller]")]
public class TestController : ControllerBase
{
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
private readonly ILogger<TestController> _logger;
public TestController(ILogger<TestController> logger)
{
_logger = logger;
}
[AllowAnonymous]
[HttpPost("login")]
public IActionResult Login()
{
var token = GenerateJwtToken("Jason");
return Ok(new { Token = token });
}
private string GenerateJwtToken(string username)
{
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("this is my custom Secret key for authentication"));
var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
var claims = new[]
{
new Claim(JwtRegisteredClaimNames.Sub, username),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
};
var token = new JwtSecurityToken(
issuer: "Auth:ValidIssuer",
audience: "Auth:Audience",
claims: claims,
expires: DateTime.Now.AddMinutes(30),
signingCredentials: credentials
);
return new JwtSecurityTokenHandler().WriteToken(token);
}
[HttpGet("test")]
public IActionResult TestMethod1()
{
return Ok("test is ok");
}
}
}
Результат испытаний
Привет @Frederik Не могли бы вы создать минимальный образец, способный воспроизвести для меня проблему? И, пожалуйста, не забудьте скрыть конфиденциальную информацию, я рассмотрю проблему позже, большое спасибо.
Привет, Джейсон, я пытался создать минимальный образец, но по какой-то причине он работает так, как задумано...
Я обновил свой вопрос, добавив дополнительную информацию. Может быть, вы видите то, чего я не вижу?
После создания нового проекта с нуля, повторного использования всех моих настроек аутентификации и развязности из исходного проекта я наконец нашел причину.
Когда я получаю доступ к своему пользовательскому интерфейсу Swagger с помощью http
: http://localhost:5173/swagger/index.html
, мой Swagger не проходит аутентификацию. Почтальон отлично работает при аутентификации на моем API с помощью http
.
Но когда я перехожу на использование swagger ui с https
: https://localhost:5272/swagger/index.html
аутентификация с помощью swagger ui работает как положено.
Благодарю за ваш ответ! К сожалению, у меня это до сих пор не работает. Я не знаю, вызывает ли у меня эту проблему использование аутентификации Firebase. А пока я остановлюсь на почтальоне...