
Basic authentication with Swagger and ASP.NET Core
Setting up basic authentication in ASP.NET Core Web API projects
Basic authentication is not so popular authentication method nowadays. There is a valid reason for that and that is mainly the way credentials are used to authenticate to access the resources. In basic authentication flow credentials are sent in every single request which makes credentials hijack a lot more easier than with other authentication flows. That is why basic authentication is recommenced to be always used with HTTPS.
To cover some basics, basic authentication flow requires client to send username and password in headers, concatenated with semicolon ":" and encoded with base64. This means every request from the client will have this header value among the others
curl --location --request GET 'http://localhost/v1/info' --header 'Authorization: Basic dXNlcm5hbWU6cGFzc3dvcmQ='
Now if you decode base64 encoded value from the header dXNlcm5hbWU6cGFzc3dvcmQ= (https://www.base64decode.net/) you will be abble to see the credentials username:password. Doesn't seem so hard to acquire credentials from any request sent to server from the client. Nevertheless basic authentication is still in use and it can be also found as part of OAuth flows. It can also be useful for machine to machine communication or for intranet resources.
In this article I will explain how to setup basic authentication for ASP.NET Core Web API project and as well how to enable basic authentication in Swagger UI so you can customize your API documentation and make it useful for testing your service endpoints.
First thing we need to do some basic Web API plumbing and return some test data which we'll later expose through Swagger UI and finally we'll protect the API with basic authentication. We are goint to return some basic current time data along with generated guid, just to distinguish responses from each request.
Initial project setup
This is sample model which populates its values on constructor
using System; namespace Sample.BasicAuth.Api.Models { public class InfoModel { public InfoModel() { Id = Guid.NewGuid().ToString(); DateTime = System.DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss"); DayOfWeek = Enum.GetName(typeof(DayOfWeek), System.DateTime.UtcNow.DayOfWeek); } public String Id { get; set; } public String DateTime { get; set; } public String DayOfWeek { get; set; } } }
Now just to return this runtime generated value throught the controller method
using Microsoft.AspNetCore.Mvc; using Sample.BasicAuth.Api.Models; namespace Sample.BasicAuth.Api.Controllers { [ApiController] [Route("[controller]")] public class InfoController : ControllerBase { [HttpGet] public ActionResult<InfoModel> Get() { return Ok(new InfoModel()); } } }
If we invoke the endpoint from POSTMAN will get the response like the following
Swagger setup
Before we add basic authentication, let's setup Swagger in the DI and pipeline of ASP.NET Core application in Startup.cs. We will need to add some NuGet packages first before we start with Swagger setup. To make things faster, you can just open your .csproj file in Visual Studio and add the packages section.
<Project Sdk="Microsoft.NET.Sdk.Web"> <PropertyGroup> <TargetFramework>netcoreapp3.1</TargetFramework> <Description>Sample.BasicAuth.Api</Description> </PropertyGroup> <ItemGroup> <PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.1.4" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.Versioning" Version="4.1.1" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer" Version="4.1.1" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="5.4.1" /> <PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="5.4.1" /> <PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="5.4.1" /> </ItemGroup> </Project>
Now when we have all references included, we need to setup dependency injection in Startup.cs
public void ConfigureServices(IServiceCollection services) { services.AddVersionedApiExplorer(options => { options.GroupNameFormat = "'v'VVV"; options.SubstituteApiVersionInUrl = true; options.AssumeDefaultVersionWhenUnspecified = true; options.DefaultApiVersion = new ApiVersion(1, 0); }); services.AddApiVersioning(options => { options.ReportApiVersions = true; options.AssumeDefaultVersionWhenUnspecified = true; options.DefaultApiVersion = new ApiVersion(1, 0); }); services.AddSwaggerGen(options => { options.EnableAnnotations(); using (var serviceProvider = services.BuildServiceProvider()) { var provider = serviceProvider.GetRequiredService<IApiVersionDescriptionProvider>(); String assemblyDescription = typeof(Startup).Assembly.GetCustomAttribute<AssemblyDescriptionAttribute>().Description; foreach (var description in provider.ApiVersionDescriptions) { options.SwaggerDoc(description.GroupName, new Microsoft.OpenApi.Models.OpenApiInfo() { Title = $"{typeof(Startup).Assembly.GetCustomAttribute<AssemblyProductAttribute>().Product} {description.ApiVersion}", Version = description.ApiVersion.ToString(), Description = description.IsDeprecated ? $"{assemblyDescription} - DEPRECATED" : $"{assemblyDescription}" }); } } }); services.AddControllers(); services.AddRouting(); }
And finally to add Swagger to ASP.NET Core pipeline
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseHttpsRedirection(); app.UseRouting(); app.UseStaticFiles(); app.UseSwagger(); var provider = app.ApplicationServices.GetService<IApiVersionDescriptionProvider>(); app.UseSwaggerUI(options => { foreach (var description in provider.ApiVersionDescriptions) { options.SwaggerEndpoint($"/swagger/{description.GroupName}/swagger.json", description.GroupName.ToUpperInvariant()); } }); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); }
Before we spin up the API and navigate to Swagger endpoint, we need to update our method in the controller first. Since Swagger is setup to distinguish API versions, we need to tell Swagger to which version our controller belongs. This is easy done with few attributes which we'll decorate controller class with.
using Microsoft.AspNetCore.Mvc; using Sample.BasicAuth.Api.Models; namespace Sample.BasicAuth.Api.Controllers { [ApiController] [ApiVersion("1.0")] [Route("v{version:apiVersion}/[controller]")] public class InfoController : ControllerBase { [HttpGet] public ActionResult<InfoModel> Get() { return Ok(new InfoModel()); } } }
Swagger UI is not ready for the test spin
At this point there is no authentivcation in place, so if we invoke the endpoint from the Swagger UI we will get proper 200 OK response with the payload of the runtime generated info model.
Basic authentication for ASP.NET Core setup
We have our API working and it is documented with Swagger. Next step is to secure the endpoints using basic authentication. Unfortunately there is no out of the box package for basic authentication so we'll have to do some stuff manually.
Before we dig into actual basic authentication wireing we need to have our service that will validate credentials against the credentials store. In most cases this would be your database or external service, but in this example, just to keep things simple we'll hard code validation logic.
using System; namespace Sample.BasicAuth.Api.Services { public interface IUserService { bool ValidateCredentials(String username, String password); } }
And interface simple implementation
namespace Sample.BasicAuth.Api.Services { public class UserService : IUserService { public bool ValidateCredentials(string username, string password) { return username.Equals("me") && password.Equals("Pa$$WoRd"); } } }
This service needs to be added to dependency injection so that authentication handler can resolve it and use it to validate credentials sent via headers using basic authentication flow
services.AddScoped<IUserService, UserService>();
Finally, authentication handler implementation which will take care of encoding header value and pass the credentials to UserService instance to validate them
namespace Sample.BasicAuth.Api { public class BasicAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions> { readonly IUserService _userService; public BasicAuthenticationHandler( IUserService userService, IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock) { _userService = userService; } protected override async Task<AuthenticateResult> HandleAuthenticateAsync() { string username = null; try { var authHeader = AuthenticationHeaderValue.Parse(Request.Headers["Authorization"]); var credentials = Encoding.UTF8.GetString(Convert.FromBase64String(authHeader.Parameter)).Split(':'); username = credentials.FirstOrDefault(); var password = credentials.LastOrDefault(); if (!_userService.ValidateCredentials(username, password)) throw new ArgumentException("Invalid credentials"); } catch(Exception ex) { return AuthenticateResult.Fail($"Authentication failed: {ex.Message}"); } var claims = new[] { new Claim(ClaimTypes.Name, username) }; var identity = new ClaimsIdentity(claims, Scheme.Name); var principal = new ClaimsPrincipal(identity); var ticket = new AuthenticationTicket(principal, Scheme.Name); return AuthenticateResult.Success(ticket); } } }
In order to have authentication in place, there are several more things to be set in place in order t engage the authentication handler to take care of the authentication in the application flow. First we need to add authentication handler to dependency injection
services.AddAuthentication("BasicAuthentication") .AddScheme<AuthenticationSchemeOptions, BasicAuthenticationHandler>("BasicAuthentication", null);
Pipeline also needs to have authentication along with authorization in place so that it can utilize the handler which we added previously to dependency injection
app.UseAuthentication(); app.UseAuthorization();
And finally, controller needs to be decorated with Authorize attribute which indicates this controller will allow only authenticated user to access its methods
namespace Sample.BasicAuth.Api.Controllers { [Authorize] [ApiController] [ApiVersion("1.0")] [Route("v{version:apiVersion}/[controller]")] public class InfoController : ControllerBase { [HttpGet] public ActionResult<InfoModel> Get() { return Ok(new InfoModel()); } } }
If you spin up your updated project and try to call GET info methods from Swagger you will get 401 Unauthenticated response
There is not option in Swagger yet to supply authentication credentials, so in order to test the authentication with our dummy hard coded credentials in UserService logic, we need to use POSTMAN with Basic Authentication header values set
This proves that our authentication logic is working as expected but we still need to enable authentication from Swagger UI
Basic authentication configuration for Swagger
Adding basic authentication handling in swagger is achieved by altering option in Swagger dependency injection registration.
services.AddSwaggerGen(options => { options.EnableAnnotations(); using (var serviceProvider = services.BuildServiceProvider()) { var provider = serviceProvider.GetRequiredService<IApiVersionDescriptionProvider>(); String assemblyDescription = typeof(Startup).Assembly.GetCustomAttribute<AssemblyDescriptionAttribute>().Description; foreach (var description in provider.ApiVersionDescriptions) { options.SwaggerDoc(description.GroupName, new Microsoft.OpenApi.Models.OpenApiInfo() { Title = $"{typeof(Startup).Assembly.GetCustomAttribute<AssemblyProductAttribute>().Product} {description.ApiVersion}", Version = description.ApiVersion.ToString(), Description = description.IsDeprecated ? $"{assemblyDescription} - DEPRECATED" : $"{assemblyDescription}" }); } } options.AddSecurityDefinition("basic", new OpenApiSecurityScheme { Name = "Authorization", Type = SecuritySchemeType.Http, Scheme = "basic", In = ParameterLocation.Header, Description = "Basic Authorization header using the Bearer scheme." }); options.AddSecurityRequirement(new OpenApiSecurityRequirement { { new OpenApiSecurityScheme { Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "basic" } }, new string[] {} } }); });
This tells Swagger to provide credentials input option so that you can set credentials in each an every request by setting them once in this dialog. Same credentials will be used in every authorized request in the same Swagger instance UI.
Once you provide credentials, you will get successful 200 OK response with runtime Info model generated payload.
Setting up good old browser pop-up for basic authentication
If you ever used Basic Authentication with ASP.NET and .NET Framework (not .NET Core) you probably remember browser dialog that pops up for credentials when you try to access the endpoint protected with basic authentication.
This is still and option and you can still use it. the secret between this browser dialog is in special headers sent from the server which browser handles by displaying the dialog asking for credentials.
If you simply return header value "WWW-Authenticate" : "Basic" in the response, browser will automatically show the dialog box for credentials. In our case this can be simply added by altering BasicAuthenticationHandler class
protected override Task HandleChallengeAsync(AuthenticationProperties properties) { Response.Headers["WWW-Authenticate"] = "Basic"; return base.HandleChallengeAsync(properties); }
This will return above mentioned headers every time credentials are not supplied in headers of the request.
Credentials popup box will appear even if you try to invoke the endpoint from the Swagger if you did not previously supplied credentials using Authorize button
References
Disclaimer
Purpose of the code contained in snippets or available for download in this article is solely for learning and demo purposes. Author will not be held responsible for any failure or damages caused due to any other usage.
Comments for this article