Swagger UI not being protected by basic authentication - c#

I have a WebApi project that generates OpenApi Swagger doc. The project already uses OIDC authentication. I need to add Basic authentication to the swagger doc, so I followed this link.
I have the following swagger config in Startup.cs:
app.UseSwaggerAuthorized();
app.UseSwagger();
app.UseSwaggerUI(c =>
{
if (env.IsDevelopment())
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API");
}
else
{
c.SwaggerEndpoint("/myapp/swagger/v1/swagger.json", "My API");
}
c.ConfigObject.AdditionalItems["syntaxHighlight"] = new Dictionary<string, object> {
["activated"] = false
};
c.RoutePrefix = string.Empty;
});
And I added the following class:
public class SwaggerBasicAuthMiddleware
{
private readonly RequestDelegate next;
public SwaggerBasicAuthMiddleware(RequestDelegate next)
{
this.next = next;
}
public async Task InvokeAsync(HttpContext context, IOptions<List<SwaggerCredential>> credentials)
{
if (context.Request.Path.StartsWithSegments("/swagger") ||
context.Request.Path.Value.ToLower().Contains("index.html"))
{
var authHeader = (string)context.Request.Headers["Authorization"];
if (authHeader != null && authHeader.StartsWith("Basic "))
{
var header = AuthenticationHeaderValue.Parse(authHeader);
var bytes = Convert.FromBase64String(header.Parameter);
var credential = Encoding.UTF8.GetString(bytes).Split(':');
var username = credential[0];
var password = credential[1];
var current = credentials.Value.SingleOrDefault(x => x.Username.Equals(username));
if (current != null && current.Password.Equals(password))
{
await next(context);
return;
}
}
context.Response.Headers["WWW-Authenticate"] = "Basic";
context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
}
else
{
await next(context);
}
}
}
In development, I access my site at: https://localhost:44333 and the swagger at: https://localhost:44333/index.html.
In production, the site is accessed at: 'https://example.com/myapp' and the swagger at: https://example.com/myapp/index.html
The dialog to enter credentials is displayed and I enter them. However, the dialog keeps reappearing.
What am I missing?

Related

c# Web API 500 error from method after adding authentication

I have been trying to fix this all day. I am making a test API to practice my development. I tried adding a bearer authentication error and now none of the methods work.
namespace WebApiTest
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; set; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddLogging(loggingBuilder =>
{
loggingBuilder.AddConsole();
});
// Register the Swagger generator, defining 1 or more Swagger documents
services.AddControllers();
services.AddSwaggerGen(c =>
{
c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme()
{
Name = "Authorization",
Type = SecuritySchemeType.Http,
Scheme = "Bearer",
BearerFormat = "JWT",
In = ParameterLocation.Header,
Description = "JWT Authorization header. \r\n\r\n Enter the token in the text input below."
});
c.OperationFilter<AddAuthorizationHeaderParameterOperationFilter>();
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILogger<Startup> logger)
{
try
{
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "Web API Test");
});
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
catch (Exception ex)
{
logger.LogError(ex, "An error occurred while configuring the application.");
throw;
}
}
}
public class AddAuthorizationHeaderParameterOperationFilter : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
var authAttributes = context.MethodInfo.DeclaringType.GetCustomAttributes(true)
.Union(context.MethodInfo.GetCustomAttributes(true))
.OfType<AuthorizeAttribute>();
if (authAttributes.Any())
{
operation.Security = new List<OpenApiSecurityRequirement>
{
new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "Bearer" },
},
new string[] {}
}
}
};
}
}
}
}
namespace AzureWebApiTest.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class MainController : ControllerBase
{
private readonly LoginRequest loginInformation = new LoginRequest("username", "password");
[HttpPost("GetToken")]
public IActionResult GetToken([FromBody] LoginRequest loginRequest)
{
if (loginRequest == null)
{
return BadRequest("Bad Login Request");
}
if (loginRequest.Equals(loginInformation))
{
var token = GenerateBearerToken(loginRequest);
return Ok(new { token });
}
else
{
return Unauthorized("Incorrect Login Information");
}
}
[HttpGet("GetHello")]
[Authorize(AuthenticationSchemes = "Bearer")]
public IActionResult GetHello([FromQuery] string name)
{
try
{
var token = Request.Headers["Authorization"].ToString().Replace("Bearer ", "");
var tokenHandler = new JwtSecurityTokenHandler();
var tokenValidationParameters = new TokenValidationParameters
{
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes("secretKey"))
};
var claimsPrincipal = tokenHandler.ValidateToken(token, tokenValidationParameters, out var securityToken);
return Ok("Hello " + name);
}
catch (SecurityTokenExpiredException)
{
return Unauthorized("Token has expired.");
}
catch (SecurityTokenInvalidSignatureException)
{
return Unauthorized("Invalid token signature.");
}
catch (Exception)
{
return Unauthorized("Invalid token.");
}
}
private string GenerateBearerToken(LoginRequest loginRequest)
{
if (ValidateCredentials(loginRequest))
{
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes("secretKey");
var tokenDescriptor = new SecurityTokenDescriptor
{
Expires = DateTime.UtcNow.AddHours(1),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
};
var token = tokenHandler.CreateToken(tokenDescriptor);
return tokenHandler.WriteToken(token);
}
return null;
}
private bool ValidateCredentials(LoginRequest loginRequest)
{
if(loginRequest.Equals(loginInformation))
{
return true;
}
return false;
}
}
}
When I try the method in swagger, I am getting the response:
I've tried getting ChatGPT to fix it but I'm getting nowhere and it's going in circles. Anyone have any ideas?
Edit:
The Validate function returns false at the end, I changed it for testing purposes. Edited back.
You are trying to get authorization to work, but you lack a few things.
Authentication
services.AddAuthentication(..options..).AddJwtBearer(..options..)
You need to add authentication to the pipeline, by adding UseAuthentication() before UseAuthorization(), like:
app.UseAuthentication();
app.UseAuthorization();
You need to add / register the authorization service using
services.AddAuthorization(..options..);

Return json from .net core api when using NotFound()

I'm trying to make my web api core return application/json, but it strangely always returns this html page breaking the error convention established by the team.
Here's the code i'm trying to execute but with no success at all so far:
Startup.cs
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddCors();
services.AddControllers().AddNewtonsoftJson(options =>
{
options.SerializerSettings.Converters.Add(new IsoDateTimeConverter { DateTimeFormat = "dd/MM/yyyy" });
});
services.AddMvcCore().AddRazorViewEngine().AddRazorRuntimeCompilation().ConfigureApiBehaviorOptions(options =>
{
options.InvalidModelStateResponseFactory = actionContext =>
{
var errorList = (from item in actionContext.ModelState
where item.Value.Errors.Any()
select item.Value.Errors[0].ErrorMessage).ToList();
return new BadRequestObjectResult(new
{
ErrorType = "bad_request",
HasError = true,
StatusCode = (int)HttpStatusCode.BadRequest,
Message = "Formato do request inválido",
Result = new
{
errors = errorList
}
});
};
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseMiddleware(typeof(ErrorHandlingMiddleware));
app.UseCors(
options => options.AllowAnyOrigin().SetIsOriginAllowed(x => _ = true).AllowAnyMethod().AllowAnyHeader()
);
app.UseHttpsRedirection();
app.UseRouting();
app.UseStaticFiles();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
AuthController.cs
[HttpPost("refreshtoken")]
public IActionResult PostRefreshToken(Guid token)
{
if (!_authTokenService.IsValid(token))
{
return NotFound(new JsonResponse
{
HasError = true,
ErrorType = "not_found",
StatusCode = (int)HttpStatusCode.NotFound,
Title = "Token não encontrado",
Message = "refresh is not valid because it was not found or does not comply",
});
}
var savedToken = _authTokenService.Get(token);
...
return Ok(new JsonResponse
{
StatusCode = (int)HttpStatusCode.OK,
Title = "Token atualizado",
Message = "jwt access token refreshed with success, please update your keys for subsequent requests",
Result = new
{
Expiration = accessToken.Expiration.ToString("dd/MM/yyyy HH:mm:ss"),
AccessToken = accessToken.Token,
RefreshToken = refreshToken.Token,
}
});
}
when this code is executed i was expecting a json result when NotFound() block is reached, but instead it returns this text/html page
ErrorHandlingMiddleware.cs
public class ErrorHandlingMiddleware
{
private readonly RequestDelegate next;
public ErrorHandlingMiddleware(RequestDelegate next)
{
this.next = next;
}
public async Task Invoke(HttpContext context /* other dependencies */)
{
try
{
await next(context);
}
catch (Exception ex)
{
await HandleExceptionAsync(context, ex);
}
}
private static Task HandleExceptionAsync(HttpContext context, Exception ex)
{
var code = HttpStatusCode.InternalServerError;
var result = JsonConvert.SerializeObject(new
{
HasError = true,
StatusCode = (int)code,
Message = ex.Message
}, new JsonSerializerSettings
{
ContractResolver = new DefaultContractResolver
{
NamingStrategy = new CamelCaseNamingStrategy()
}
});
context.Response.ContentType = "application/json";
context.Response.StatusCode = (int)code;
return context.Response.WriteAsync(result);
}
}
In my case it ended up being the visual studio extension Conveyor by Keyoti being the culprit of the errors aforementioned.
When i disabled the extension, the code was revealed to be ok and returning the right code, a json object body sent by the server.

Use id_token with ASP.NET Core 3

In an ASP.NET Core 3 application, I need to process information from id_token along with access_token.
The id_token has membership information that is sometimes required to build a policy. Since the membership information can be large, making it part of the access_token is not possible (token exceeds maximum allowed size).
The clients send id_token in x-id-token header and I am looking for a way to extract it and use the claims within.
Right now I have JwtBearer auth configured which works seamlessly with Authorization: Bearer access_token header.
public void ConfigureServices(IServiceCollection services) {
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.Authority = $"https://{Configuration["auth:Domain"]}/";
options.Audience = Configuration["auth:Audience"];
});
...
}
AS stated in the question, I needed a step in authorization flow to validate id_token and a membership_id supplied in custom headers. I ended up creating a custom auth requirement handler in the following form
internal class MembershipRequirement : AuthorizationHandler<MembershipRequirement>, IAuthorizationRequirement
{
public MembershipRequirement(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, MembershipRequirement requirement)
{
var authFilterCtx = (Microsoft.AspNetCore.Mvc.Filters.AuthorizationFilterContext)context.Resource;
string idToken = authFilterCtx.HttpContext.Request.Headers["x-id-token"];
string membershipId = authFilterCtx.HttpContext.Request.Headers["x-selected-membership-id"];
if (idToken != null && membershipId != null)
{
var identity = ValidateIdToken(idToken).Result;
if (identity != null)
{
var subscriptions = identity.Claims.ToList().FindAll(s => s.Type == "https://example.com/subs").ToList();
var assignments = subscriptions.Select(s => JsonSerializer.Deserialize<Subscription>(s.Value)).ToList();
var membership = assignments.Find(a => a.id == membershipId);
if (membership != null)
{
// assign the id token claims to user identity
context.User.AddIdentity(new ClaimsIdentity(identity.Claims));
context.Succeed(requirement);
}
else { context.Fail(); }
}
else
{
context.Fail();
}
}
return Task.FromResult<object>(null);
}
private async Task<ClaimsPrincipal> ValidateIdToken(string token)
{
try
{
IConfigurationManager<OpenIdConnectConfiguration> configurationManager = new ConfigurationManager<OpenIdConnectConfiguration>($"https://{Configuration["Auth:Domain"]}/.well-known/openid-configuration", new OpenIdConnectConfigurationRetriever());
OpenIdConnectConfiguration openIdConfig = await configurationManager.GetConfigurationAsync(CancellationToken.None);
TokenValidationParameters validationParameters =
new TokenValidationParameters
{
IssuerSigningKeys = openIdConfig.SigningKeys,
ValidateIssuer = false,
ValidateAudience = false
};
var validator = new JwtSecurityTokenHandler();
SecurityToken validatedToken;
var identity = validator.ValidateToken(token, validationParameters, out validatedToken);
return identity;
}
catch (Exception e)
{
Console.Writeline($"Error occurred while validating token: {e.Message}");
return null;
}
}
}
internal class Subscription
{
public string name { get; set; }
public string id { get; set; }
}
Then in the public void ConfigureServices(IServiceCollection services) method added a policy to check for membership in the id_token
services.AddAuthorization(options =>
{
options.AddPolicy("RequiredCompanyMembership", policy => policy.Requirements.Add(new MembershipRequirement(Configuration)));
});
For us, this policy is globally applied for all Authorized endpoints.

.Net Core Custom Authentication using API Keys with Identity Server 4

I have a .NET Core 2.2 Web API that authenticates with JWT tokens. Tokens are generated by Identity Server 4 on a separate API.
All the authentication and authorisation works as expected with JWT tokens. But I need to extend this to allow usage of API keys. If an API key is supplied, I want to load up the claims of that particular user, add it to the request and let Authorize attribute deal with the set policies.
Here is what I have done so far following suggestions from here. My error is exactly the same as the linked post and it works for me as well using GenericPrincipal with a set of roles but I am using AuthorisationPolicies and I always get 401 error with my current implementation, giving me errors similar to the link above.
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddMvcCore(options =>
{
options.Filters.Add(new RequireHttpsAttribute());
options.Filters.Add(new AuthorizeFilter());
options.Filters.Add(typeof(ValidateModelStateAttribute));
options.AllowEmptyInputInBodyModelBinding = true;
})
.AddAuthorization(options =>
{
options.AddPolicies();
})
.AddJsonFormatters();
services.AddAuthentication(IdentityServerAuthenticationDefaults.AuthenticationScheme)
.AddIdentityServerAuthentication(options =>
{
options.Authority = Configuration["Authentication:Authority"];
options.RequireHttpsMetadata = true;
options.ApiName = Configuration["Authentication:ApiName"];
});
services.AddCors();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
// 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.UseCors(policy =>
{
policy.AllowAnyHeader();
policy.AllowAnyMethod();
policy.AllowAnyOrigin();
});
app.UseHttpsRedirection();
app.UseMiddleware<ApiKeyMiddleware>();
app.UseAuthentication();
app.UseMvc();
}
AuthorizationPolicies.cs
public static class AuthorizationPolicies
{
public const string ReadUsersPolicy = "ReadUsers";
public const string EditUsersPolicy = "EditUsers";
public static void AddPolicies(this AuthorizationOptions options)
{
options.AddPolicy(ReadUsersPolicy, policy => policy.RequireClaim(Foo.Permission, Foo.CanReadUsers));
options.AddPolicy(EditUsersPolicy, policy => policy.RequireClaim(Foo.Permission, Foo.CanEditUsers));
}
}
ApiKeyMiddleware
public class ApiKeyMiddleware
{
public ApiKeyMiddleware(RequestDelegate next)
{
_next = next;
}
private readonly RequestDelegate _next;
public async Task Invoke(HttpContext context)
{
if (context.Request.Path.StartsWithSegments(new PathString("/api")))
{
if (context.Request.Headers.Keys.Contains("ApiKey", StringComparer.InvariantCultureIgnoreCase))
{
var headerKey = context.Request.Headers["ApiKey"].FirstOrDefault();
await ValidateApiKey(context, _next, headerKey);
}
else
{
await _next.Invoke(context);
}
}
else
{
await _next.Invoke(context);
}
}
private async Task ValidateApiKey(HttpContext context, RequestDelegate next, string key)
{
var userClaimsService = context.RequestServices.GetService<IUserClaimsService>();
List<string> permissions = (await userClaimsService.GetAllPermissionsForApiKey(key))?.ToList();
if (permissions == null)
{
context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
await context.Response.WriteAsync("Invalid Api Key");
return;
}
ICollection<Claim> claims = permissions.Select(x => new Claim(FooClaimTypes.Permission, x)).ToList();
var identity = new ClaimsIdentity(claims);
var principal = new ClaimsPrincipal(identity);
context.User = principal;
await next.Invoke(context);
}
}
UsersController.cs
[Authorize(AuthorizationPolicies.EditUsersPolicy)]
public async Task<IActionResult> Put([FromBody] UserUpdateDto userUpdateDto)
{
...
}
Apparently, I had to set AuthenticationType to be Custom on the ClaimsIdentity as explained here.
var identity = new ClaimsIdentity(claims, "Custom");

How to protect static folder in asp.net core 2.1 using claims-based authorization

I have a small project which uses asp.net core 2.1. I wish to protect folder full of static assets. I tried to implement is based on this article https://odetocode.com/blogs/scott/archive/2015/10/06/authorization-policies-and-middleware-in-asp-net-5.aspx
I am using cookies and claims-based authorization. All view which are supposed to check authorization work fine... except static folder. When I check httpContext.User it is missing all expected claims.
Middleware:
public class ProtectFolder
{
private readonly RequestDelegate _next;
private readonly PathString _path;
private readonly string _policyName;
public ProtectFolder(RequestDelegate next, ProtectFolderOptions options)
{
_next = next;
_path = options.Path;
_policyName = options.PolicyName;
}
public async Task Invoke(HttpContext httpContext, IAuthorizationService authorizationService)
{
if (httpContext.Request.Path.StartsWithSegments(_path))
{
var authorized = await authorizationService.AuthorizeAsync(httpContext.User, null, _policyName);
if (!authorized.Succeeded)
{
await httpContext.ChallengeAsync();
return;
}
}
await _next(httpContext);
}
}
Startup.cs
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.Configure<CookiePolicyOptions>(options =>
{
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddMvc()
.AddRazorPagesOptions(options =>
{
options.Conventions.AuthorizePage("/Contact");
})
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie();
services.AddAuthorization(options =>
{
options.AddPolicy("Authenticated", policy => policy.RequireAuthenticatedUser());
});
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
else
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseCookiePolicy();
app.UseProtectFolder(new ProtectFolderOptions
{
Path = "/Docs",
PolicyName = "Authenticated"
});
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), "Docs")),
RequestPath = "/Docs",
});
app.UseAuthentication();
app.UseMvc();
}
}
Login is very simple. Just set the cookie, when authenticate
public async Task<IActionResult> OnGetAsync(string returnUrl = null)
{
ReturnUrl = returnUrl;
if (ModelState.IsValid)
{
var user = await AuthenticateUser("aaa");
if (user == null)
{
ModelState.AddModelError(string.Empty, "Invalid login attempt.");
return Page();
}
var claims = new List<Claim>
{
new Claim(ClaimTypes.Name, user.Email),
new Claim("FullName", user.FullName),
new Claim(ClaimTypes.Role, "Administrator"),
};
var claimsIdentity = new ClaimsIdentity(
claims, CookieAuthenticationDefaults.AuthenticationScheme);
var authProperties = new AuthenticationProperties
{
};
await HttpContext.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme,
new ClaimsPrincipal(claimsIdentity),
authProperties);
_logger.LogInformation($"User {user.Email} logged in at {DateTime.UtcNow}.");
return LocalRedirect(Url.GetLocalUrl(returnUrl));
}
return Page();
}
private async Task<ApplicationUser> AuthenticateUser(string token)
{
await Task.Delay(500);
if (token == "aaa")
{
return new ApplicationUser()
{
Email = "aaa#gmail.com",
FullName = "aaa"
};
}
else
{
return null;
}
}
Once again. It works for all pages which require authentication except static folder. What am I doing wrong?
app.UseAuthentication(); //<-- this should go first
app.UseProtectFolder(new ProtectFolderOptions
{
Path = "/Docs",
PolicyName = "Authenticated"
});
Calling UseStaticFiles() first will short-cut the pipeline for static files. So no authentication are done on the static files.
More info on Startup.Configure order here:
https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware/?view=aspnetcore-2.1#order

Categories