GetRouteData always null using AspNetCore.OData 7.2.1 - c#

I am trying to secure an OData api using .net Core 2.2 and AspNetCore.OData 7.2.1, with a basic authentication handler.
I need to handler multi tenant urls, and retrieve from the uri the token that will then be used in the authorization handler to determine if a user is authorized.
To do so I use the IHttpContextAccessor but this works only with standard api, and not with OData.
OData does not like EndpointRouting and I had to disable it as shown below, but in this case then how can I access the RouteData to take the tenant token?
Is there an alternative approach? Below the code you can use to try this out.
Startup.cs
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddHttpContextAccessor();
services.AddAuthentication("BasicAuthentication")
.AddScheme<AuthenticationSchemeOptions, BasicAuthenticationHandler>("BasicAuthentication", null);
services.AddMvc(options => options.EnableEndpointRouting = false)
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
services.AddOData();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
// Needed to be able to get RouteData from HttpContext through the IHttpContextAccessor
app.UseEndpointRouting();
// Needed to secure the application using the standard Authorize attribute
app.UseAuthentication();
// OData entity model builder
var builder = new ODataConventionModelBuilder(app.ApplicationServices);
builder.EntitySet<Value>(nameof(Value) + "s");
app.UseMvc();
app.UseOData("odata", "{tenant}/odata", builder.GetEdmModel());
// Alternative configuration which is affected by the same problem
//
// app.UseMvc(routeBuilder =>
// {
// // Map OData routing adding token for the tenant based url
// routeBuilder.MapODataServiceRoute("odata", "{tenant}/odata", builder.GetEdmModel());
//
// // Needed to allow the injection of OData classes
// routeBuilder.EnableDependencyInjection();
// });
}
BasicAuthenticationHandler.cs
public class BasicAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
private readonly IHttpContextAccessor _httpContextAccessor;
public BasicAuthenticationHandler(
IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock,
IHttpContextAccessor httpContextAccessor)
: base(options, logger, encoder, clock)
{
_httpContextAccessor = httpContextAccessor;
}
public string GetTenant()
{
var httpContext = _httpContextAccessor?.HttpContext;
var routeData = httpContext?.GetRouteData(); // THIS RESULTS ALWAYS IN NULL ROUTE DATA!
return routeData?.Values["tenant"]?.ToString();
}
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
if (!Request.Headers.ContainsKey("Authorization"))
return AuthenticateResult.Fail("Missing Authorization Header");
try {
var authHeader = AuthenticationHeaderValue.Parse(Request.Headers["Authorization"]);
var credentialBytes = Convert.FromBase64String(authHeader.Parameter);
var credentials = Encoding.UTF8.GetString(credentialBytes).Split(':');
var username = credentials[0];
var password = credentials[1];
var tenant = GetTenant();
if (string.IsNullOrEmpty(tenant))
{
return AuthenticateResult.Fail("Unknown tenant");
}
if(string.IsNullOrEmpty(username) || username != password)
return AuthenticateResult.Fail("Wrong username or password");
}
catch (Exception e)
{
return AuthenticateResult.Fail("Unable to authenticate");
}
var claims = new[] {
new Claim("Tenant", "tenant id")
};
var identity = new ClaimsIdentity(claims, Scheme.Name);
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, Scheme.Name);
return AuthenticateResult.Success(ticket);
}
protected override async Task HandleChallengeAsync(AuthenticationProperties properties)
{
Response.Headers["WWW-Authenticate"] = "Basic realm=\"Oh my OData\", charset=\"UTF-8\"";
await base.HandleChallengeAsync(properties);
}
}
Value.cs
public class Value
{
public int Id { get; set; }
public string Name { get; set; }
}
ValuesController.cs
[Authorize]
public class ValuesController : ODataController
{
private List<Value> _values;
public ValuesController()
{
_values = new List<Value>
{
new Value {Id = 1, Name = "A1"},
new Value {Id = 2, Name = "A2"},
new Value {Id = 3, Name = "A3"},
new Value {Id = 4, Name = "A4"},
new Value {Id = 5, Name = "A5"},
new Value {Id = 6, Name = "A6"},
new Value {Id = 7, Name = "A7"},
new Value {Id = 11, Name = "B1"},
new Value {Id = 12, Name = "B2"},
new Value {Id = 13, Name = "B3"},
new Value {Id = 14, Name = "B4"},
new Value {Id = 15, Name = "B5"},
new Value {Id = 16, Name = "B6"},
new Value {Id = 17, Name = "B7"}
};
}
// GET {tenant}/odata/values
[EnableQuery]
public IQueryable<Value> Get()
{
return _values.AsQueryable();
}
// GET {tenant}/odata/values/5
[EnableQuery]
public ActionResult<Value> Get([FromODataUri] int key)
{
if(_values.Any(v => v.Id == key))
return _values.Single(v => v.Id == key);
return NotFound();
}
}
EDIT:
Added sample code in working application to reproduce the issue and test solutions: https://github.com/norcino/so-58016881-OData-GetRoute

OData does not like EndpointRouting and I had to disable it as shown below, but in this case then how can I access the RouteData to take the tenant token?
As you know, OData doesn't work fine with ASP.NET Core 2.2 EndPoint Routing. For more details at present, see https://github.com/OData/WebApi/issues/1707
var routeData = httpContext?.GetRouteData(); // THIS RESULTS ALWAYS IN NULL ROUTE DATA!
The reason why you always get a null route data is that the Authentication middleware runs before the Router middleware takes effect. In other words, you won't get route data before the Router middleware is invoked.
To walk around it, just create a router and make it runs before the Authentication middleware.
How to fix
Make sure you've disabled the EnableEndpointRouting:
services.AddMvc(
options => options.EnableEndpointRouting = false
)
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
Remove the line of app.UseEndpointRouting():
// OData doesn't work fine with ASP.NET Core 2.2 EndPoint Routing, See https://github.com/OData/WebApi/issues/1707
// app.UseEndpointRouting();
Set up a Router before Authentication so that you can get the Route Data within AuthenticationHandler later:
// configure Routes for OData
app.UseRouter(routeBuilder =>{
var templatePrefix="{tenant}/odata";
var template = templatePrefix + "/{*any}";
routeBuilder.MapMiddlewareRoute(template, appBuilder =>{
var builder = new ODataConventionModelBuilder(app.ApplicationServices);
builder.EntitySet<Value>(nameof(Value) + "s");
appBuilder.UseAuthentication();
appBuilder.UseMvc();
appBuilder.UseOData("odata", templatePrefix, builder.GetEdmModel());
});
});
// ... add more middlewares if you want other MVC routes
app.UseAuthentication();
app.UseMvc(rb => {
rb.MapRoute("default","{controller=Home}/{action=Index}/{id?}");
});
Demo
Send a request to the Values API
GET https://localhost:5001/msft/odata/values
Authorization: Basic dGVzdDp0ZXN0
And then we'll get the route data as below:

Related

OpenIddict Password Grant returns Status Code 415

Good afternoon,
So I am working on integrating OpenIddict into an existing project. I have become stuck on the issuing of the token because I am unable to debug the issue. So I am getting a two entirely different responses from the controller. I have tried this through swagger and postman both and gotten the same Status: 415 Unsupported Media Type. Now I was pretty sure I read that the information coming to the token for the password grant had to be "application/x-www-form-urlencoded" So that is what I am passing to the controller, but still receiving a 415. On the flip side of that if I look in the debug log for the project I see the following:
OpenIddict.Server.OpenIddictServerDispatcher: Information: The request address matched a server endpoint: Token.
OpenIddict.Server.OpenIddictServerDispatcher: Information: The token request was successfully extracted: {
"grant_type": "password",
"username": "Administrator#MRM2Inc.com",
"password": "[redacted]"
}.
OpenIddict.Server.OpenIddictServerDispatcher: Information: The token request was successfully validated.
That too me looks like it was working. It never went into the token endpoint as I had a breakpoint immediately set.
Here is my setup:
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddDbContext<IdentDbContext>(options =>
{
//options.UseSqlServer(
// Configuration.GetConnectionString("IdentityDB"));
options.UseOpenIddict<Guid>();
});
// Add the Identity Services we are going to be using the Application Users and the Application Roles
services.AddIdentity<ApplicationUsers, ApplicationRoles>(config =>
{
config.SignIn.RequireConfirmedEmail = true;
config.SignIn.RequireConfirmedAccount = true;
config.User.RequireUniqueEmail = true;
config.Lockout.MaxFailedAccessAttempts = 3;
}).AddEntityFrameworkStores<IdentDbContext>()
.AddUserStore<ApplicationUserStore>()
.AddRoleStore<ApplicationRoleStore>()
.AddRoleManager<ApplicationRoleManager>()
.AddUserManager<ApplicationUserManager>()
.AddErrorDescriber<ApplicationIdentityErrorDescriber>()
.AddDefaultTokenProviders()
.AddDefaultUI();
services.AddDataLibrary();
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme;
});
//services.AddSingleton<IGenerateTokens, GenerateTokens>();
// Add in the email
var emailConfig = Configuration.GetSection("EmailConfiguration").Get<EmailConfiguration>();
services.AddSingleton(emailConfig);
services.AddEmailLibrary();
services.AddOpenIddict()
// Register the OpenIddict core components.
.AddCore(options =>
{
// Configure OpenIddict to use the Entity Framework Core stores and models.
// Note: call ReplaceDefaultEntities() to replace the default entities.
options.UseEntityFrameworkCore()
.UseDbContext<IdentDbContext>()
.ReplaceDefaultEntities<Guid>();
})
// Register the OpenIddict server components.
.AddServer(options =>
{
// Enable the token endpoint. What other endpoints?
options.SetTokenEndpointUris("/Token");
// Enable the client credentials flow. Which flow do I need?
options.AllowPasswordFlow();
options.AcceptAnonymousClients();
// Register the signing and encryption credentials.
options.AddDevelopmentEncryptionCertificate()
.AddDevelopmentSigningCertificate();
// Register the ASP.NET Core host and configure the ASP.NET Core options.
options.UseAspNetCore()
.EnableTokenEndpointPassthrough();
})
// Register the OpenIddict validation components.
.AddValidation(options =>
{
// Import the configuration from the local OpenIddict server instance.
options.UseLocalServer();
// Register the ASP.NET Core host.
options.UseAspNetCore();
});
// Register the Swagger generator, defining 1 or more Swagger documents
services.AddSwaggerGen(swagger =>
{
swagger.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme()
{
Name = "Authorization",
Type = SecuritySchemeType.Http,
Scheme = "Bearer",
BearerFormat = "JWT",
In = ParameterLocation.Header,
Description = "JWT Authorization header using the Bearer scheme. \r\n\r\n Enter 'Bearer'[space] and then your token in the text input below.\r\n\r\nExample: \"Bearer 12345abcdef\""
});
swagger.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
}
},
new string[] {}
}
});
swagger.OperationFilter<SwaggerDefaultValues>();
swagger.OperationFilter<AuthenticationRequirementOperationFilter>();
// Set the comments path for the Swagger JSON and UI.
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
swagger.IncludeXmlComments(xmlPath);
});
services.AddApiVersioning();
services.AddVersionedApiExplorer(options =>
{
options.GroupNameFormat = "'v'VVVV";
options.DefaultApiVersion = ApiVersion.Parse("0.6.alpha");
options.AssumeDefaultVersionWhenUnspecified = true;
});
services.AddTransient<IConfigureOptions<SwaggerGenOptions>, ConfigureSwaggerOptions>();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IApiVersionDescriptionProvider provider)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
// 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.UseHttpsRedirection();
app.UseStaticFiles();
// Enable middleware to serve generated Swagger as a JSON endpoint.
app.UseSwagger();
// Enable middleware to serve swagger-ui (HTML, JS, CSS, etc.),
// specifying the Swagger JSON endpoint.
app.UseSwaggerUI(c =>
{
c.DisplayOperationId();
var versionDescription = provider.ApiVersionDescriptions;
foreach (var description in provider.ApiVersionDescriptions.OrderByDescending(_ => _.ApiVersion))
{
c.SwaggerEndpoint($"/swagger/{description.GroupName}/swagger.json", $"MRM2 Identity API {description.GroupName}");
}
});
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
AuthorizationController.cs:
[Route("api/[controller]/[action]")]
[ApiController]
[ApiVersion("0.8.alpha")]
[Produces(MediaTypeNames.Application.Json)]
[Consumes(MediaTypeNames.Application.Json)]
public class AuthorizationController : ControllerBase
{
private readonly IConfiguration _configuration;
private readonly IdentDbContext _context;
private readonly ApplicationUserManager _userManager;
private readonly ApplicationRoleManager _roleManager;
private readonly IGenerateTokens _tokens;
private readonly IOpenIddictApplicationManager _applicationManager;
private readonly IOpenIddictAuthorizationManager _authorizationManager;
private readonly IOpenIddictScopeManager _scopeManager;
private readonly SignInManager<ApplicationUsers> _signInManager;
private HttpClient _client;
public AuthorizationController(IConfiguration configuration, IdentDbContext context, ApplicationUserManager userManager,
ApplicationRoleManager roleManager, IGenerateTokens tokens, IOpenIddictApplicationManager applicationManager, IOpenIddictAuthorizationManager authorizationManager,
IOpenIddictScopeManager scopeManager, SignInManager<ApplicationUsers> signInManager)
{
_configuration = configuration;
_context = context;
_userManager = userManager;
_roleManager = roleManager;
_tokens = tokens;
_applicationManager = applicationManager;
_authorizationManager = authorizationManager;
_scopeManager = scopeManager;
_signInManager = signInManager;
}
[HttpPost("/token"), Produces("application/json")]
public async Task<IActionResult> Exchange()
{
var request = HttpContext.GetOpenIddictServerRequest() ??
throw new InvalidOperationException("The OpenID Connect request cannot be retrieved."); //does not even hit this breakpoint.
ClaimsPrincipal claimsPrincipal;
if (request.IsPasswordGrantType())
{
var user = await _userManager.FindByNameAsync(request.Username);
var roleList = await _userManager.GetRolesListAsync(user);
var databaseList = await _userManager.GetDatabasesAsync(user);
string symKey = _configuration["Jwt:Symmetrical:Key"];
string jwtSub = _configuration["Jwt:Subject"];
string issuer = _configuration["Jwt:Issuer"];
string audience = _configuration["Jwt:Audience"];
var claims = new List<Claim>
{
new Claim(JwtRegisteredClaimNames.Sub, jwtSub, issuer),
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString(), issuer),
new Claim(ClaimTypes.Name, user.UserName, issuer)
};
foreach (var role in roleList)
{
claims.Add(new Claim(ClaimTypes.Role, role.Name));
}
foreach (var database in databaseList)
{
claims.Add(new Claim(type: "DatabaseName", database));
}
var identity = new ClaimsIdentity(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
identity.AddClaim(OpenIddictConstants.Claims.Name, user.UserName, OpenIddictConstants.Destinations.AccessToken);
identity.AddClaim(OpenIddictConstants.Claims.Subject, jwtSub, OpenIddictConstants.Destinations.AccessToken);
identity.AddClaim(OpenIddictConstants.Claims.Audience, audience, OpenIddictConstants.Destinations.AccessToken);
foreach (var cl in claims)
{
identity.AddClaim(cl.Type, cl.Value, OpenIddictConstants.Destinations.AccessToken);
}
claimsPrincipal = new ClaimsPrincipal(identity);
}
if (request.IsAuthorizationCodeGrantType() || request.IsRefreshTokenGrantType())
{
// Retrieve the claims principal stored in the authorization code/device code/refresh token.
var principal = (await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)).Principal;
// Retrieve the user profile corresponding to the authorization code/refresh token.
// Note: if you want to automatically invalidate the authorization code/refresh token
// when the user password/roles change, use the following line instead:
// var user = _signInManager.ValidateSecurityStampAsync(info.Principal);
var user = await _userManager.GetUserAsync(principal);
if (user == null)
{
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The token is no longer valid."
}));
}
// Ensure the user is still allowed to sign in.
if (!await _signInManager.CanSignInAsync(user))
{
return Forbid(
authenticationSchemes: OpenIddictServerAspNetCoreDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIddictServerAspNetCoreConstants.Properties.Error] = Errors.InvalidGrant,
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The user is no longer allowed to sign in."
}));
}
foreach (var claim in principal.Claims)
{
claim.SetDestinations(GetDestinations(claim, principal));
}
// Returning a SignInResult will ask OpenIddict to issue the appropriate access/identity tokens.
return SignIn(principal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
throw new InvalidOperationException("The specified grant type is not supported.");
}
private IEnumerable<string> GetDestinations(Claim claim, ClaimsPrincipal principal)
{
// Note: by default, claims are NOT automatically included in the access and identity tokens.
// To allow OpenIddict to serialize them, you must attach them a destination, that specifies
// whether they should be included in access tokens, in identity tokens or in both.
switch (claim.Type)
{
case Claims.Name:
yield return Destinations.AccessToken;
if (principal.HasScope(Scopes.Profile))
yield return Destinations.IdentityToken;
yield break;
case Claims.Email:
yield return Destinations.AccessToken;
if (principal.HasScope(Scopes.Email))
yield return Destinations.IdentityToken;
yield break;
case Claims.Role:
yield return Destinations.AccessToken;
if (principal.HasScope(Scopes.Roles))
yield return Destinations.IdentityToken;
yield break;
// Never include the security stamp in the access and identity tokens, as it's a secret value.
case "AspNet.Identity.SecurityStamp": yield break;
default:
yield return Destinations.AccessToken;
yield break;
}
}
}
Here is how I have postman set for this, you can see the 415 return
Not sure what I missed in the setup, but I am receiving conflicting information, plus I want to troubleshoot the controller to determine what else I need to finish off the controller and ensure I am getting the information that I is needed for the rest of the program. The 415 is not allowing me to troubleshoot the issue, but something is saying that the information is correct?
[HttpPost("/token"), Produces("application/json")]
public async Task<IActionResult> Exchange()
According to the above code, since you are setting the Produces attribute as application/json, when you send a request using PostMan, try to set the Content-Type as application/json (in the request's Body panel, select Raw and choose JSON format).
Refer the follow sample screenshot:
Besides, I also tried to change the Produces attribute to application/x-www-form-urlencoded, but it will show http 406 error or 415 error. So, try to use the above method and set the Content-Type as application/json.
Turns out the issue was at the head of the controller. The previous answer helped me see that while they saw the produces("application/json"), it was not exactly that. At the top of the controller I had a Consumes("application/json"). Removed this and it went into the method.

.net 5 how to setup authorization using custom provider

in my .net 5 website i have to read user login from header and the call external webservice to check if is authorized and get permission list.
EDIT 3:
GOALS
Read current user from http header setted by corporate single sign-on
Read user permission and info by calling external web services and
keep them daved to prevent extra-calls for every action
let the user be free to access by any page
authorize by default all controller's actions with custom claims
Actual Problem
context.User.Identity.IsAuthenticated in middleware is always false
Actual code
Startup - ConfigureServices
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie();
services.AddControllers(options => { options.Filters.Add<AuditAuthorizationFilter>(); });
services.AddSession(options =>
{
options.IdleTimeout = TimeSpan.FromSeconds(10);
options.Cookie.HttpOnly = true;
options.Cookie.IsEssential = true;
});
Startup - Configure
app.UseMiddleware<AuthenticationMiddleware>();
app.UseAuthentication();
app.UseAuthorization();
Middleware
public class AuthenticationMiddleware
{
private readonly RequestDelegate _next;
// Dependency Injection
public AuthenticationMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context)
{
if (!context.User.Identity.IsAuthenticated)
{
var claims = new List<Claim>
{
new Claim(ClaimTypes.Name, context.Request.Headers["Token"]),
};
var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaultsAuthenticationScheme);
var authProperties = new AuthenticationProperties();
await context.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme,
new ClaimsPrincipal(claimsIdentity),
authProperties);
}
await _next(context);
}
}
Filter
public class AuditAuthorizationFilter : IAuthorizationFilter, IOrderedFilter
{
public int Order => -1;
private readonly IHttpContextAccessor _httpContextAccessor;
public AuditAuthorizationFilter(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
public void OnAuthorization(AuthorizationFilterContext context)
{
if (context.HttpContext.User.Identity.IsAuthenticated)
{
context.Result = new ForbidResult();
}
else
{
string metodo = $"{context.RouteData.Values["controller"]}/{context.RouteData.Values["action"]}";
if (!context.HttpContext.User.HasClaim("type", metodo))
{
context.Result = new ForbidResult();
}
}
}
}
EDIT 2:
my Startup
public void ConfigureServices(IServiceCollection services)
{
services.AddDevExpressControls();
services.AddTransient<ILoggingService, LoggingService>();
services.AddHttpContextAccessor();
services.AddMvc().SetCompatibilityVersion(Microsoft.AspNetCore.Mvc.CompatibilityVersion.Version_3_0);
services.ConfigureReportingServices(configurator => {
configurator.UseAsyncEngine();
configurator.ConfigureWebDocumentViewer(viewerConfigurator => {
viewerConfigurator.UseCachedReportSourceBuilder();
});
});
services.AddControllersWithViews().AddJsonOptions(options => options.JsonSerializerOptions.PropertyNamingPolicy = null);
services.AddControllersWithViews().AddRazorRuntimeCompilation();
services.AddControllers(options => { options.Filters.Add(new MyAuthenticationAttribute ()); });
services.AddDistributedMemoryCache();
services.AddSession(options =>
{
options.IdleTimeout = TimeSpan.FromSeconds(10);
options.Cookie.HttpOnly = true;
options.Cookie.IsEssential = true;
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory)
{
app.UseDevExpressControls();
app.UseExceptionHandlerMiddleware(Log.Logger, errorPagePath: "/Error/HandleError" , respondWithJsonErrorDetails: true);
app.UseStatusCodePagesWithReExecute("/Error/HandleError/{0}");
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseSerilogRequestLogging(opts => opts.EnrichDiagnosticContext = LogHelper.EnrichFromRequest);
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseSession();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
EDIT 1:
to adapt original code to .net 5 i made some changes:
if (!context.HttpContext.User.Identity.IsAuthenticated)
{
const string MyHeaderToken = "HTTP_KEY";
string userSSO = null;
if (string.IsNullOrWhiteSpace(context.HttpContext.Request.Headers[MyHeaderToken]))
{
userSSO = context.HttpContext.Request.Headers[MyHeaderToken];
}
if (string.IsNullOrWhiteSpace(userSSO))
{
//filterContext.Result = new unh();
}
else
{
// Create GenericPrincipal
GenericIdentity webIdentity = new GenericIdentity(userSSO, "My");
//string[] methods = new string[0]; // GetMethods(userSSO);
GenericPrincipal principal = new GenericPrincipal(webIdentity, null);
IdentityUser user = new (userSSO);
Thread.CurrentPrincipal = principal;
}
}
but context.HttpContext.User.Identity.IsAuthenticated is false everytimes, even if the previous action set principal
ORIGINAL:
I'using custom attribute to manage this scenario in this way:
public class MyAuthenticationAttribute : ActionFilterAttribute, IAuthenticationFilter{
public string[] Roles { get; set; }
public void OnAuthentication(AuthenticationContext filterContext)
{
string MyHeaderToken = “SM_USER”;
string userSSO = null;
if (HttpContext.Current.Request.Headers[MyHeaderToken] != null)
{
userSSO = HttpContext.Current.Request.Headers[MyHeaderToken];
Trace.WriteLine(string.Format(“got MyToken: {0}”, userSSO));
}
if (string.IsNullOrWhiteSpace(userSSO))
{
Trace.WriteLine(“access denied, no token found”);
}
else
{
// Create GenericPrincipal
GenericIdentity webIdentity = new GenericIdentity(userSSO, “My”);
string[] methods= GetMethods(userSSO);
GenericPrincipal principal = new GenericPrincipal(webIdentity, methods);
filterContext.HttpContext.User = principal;
}
}
public void OnAuthenticationChallenge(AuthenticationChallengeContext filterContext)
{
//check authorizations
}
}
but external webservice returns list of controller/action authorized for users, so i have to test all actions executions to simply check if names is contained in the list.
is there a way to do this without have to write attribute on every actions or every controllers in this way:
[MyAuthentication(Roles = “Admin”)]
pubic class AdminController: Controller
{
}
i know i can use
services.AddMvc(o =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
o.Filters.Add(new AuthorizeFilter(policy));
});
but no idea of how to use this with my custom authorization
i'am also not sure if string[] methods= GetMethods(userSSO) is cached by .net core filterContext.HttpContext.User avoiding multiple calls to external webservice.
Thanks
If you want to apply your custom IAuthenticationFilter globally then you can do the ff:
services.AddControllers(options =>
{
options.Filters.Add(new MyAuthenticationFilter());
});
With this approach, you no longer need to inherit from ActionFilterAttribute and no need to add the [MyAuthentication(Roles = “Admin”)] attributes.
Just ensure that you are allowing anonymous requests to actions that doesn't need authentication and/or authorization.
EDIT 2:
For your updated setup, make sure you do the ff:
Add cookie authentication
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) .AddCookie();
Order of middlewares
app.UseRouting();
app.UseAuthentication();
app.UseMiddleware<AuthenticationMiddleware>();
app.UseAuthorization();
EDIT 1:
i'am also not sure if string[] methods= GetMethods(userSSO) is cached by .net core filterContext.HttpContext.User avoiding multiple calls to external webservice.
The lifetime of the filter depends on how you implemented it, usually it is singleton but you can make it transient by following the approach below:
public class MyAuthorizationFilter : IAuthorizationFilter, IOrderedFilter
{
public int Order => -1; // Ensures that it runs first before basic Authorize filter
public void OnAuthorization(AuthorizationFilterContext context)
{
if (!context.HttpContext.User.Identity.IsAuthenticated)
{
if (context.HttpContext.Session.IsAvailable
&& context.HttpContext.Session.TryGetValue("_SessionUser", out byte[] _user))
{
SessionUser su = (SessionUser)this.ByteArrayToObject(_user);
GenericPrincipal principal = this.CreateGenericPrincipal(su.IdentityName, su.Type, su.Roles);
context.HttpContext.User = principal;
}
else
{
const string MyHeaderToken = "HTTP_KEY";
string userSSO = null;
if (!string.IsNullOrWhiteSpace(context.HttpContext.Request.Headers[MyHeaderToken]))
{
userSSO = context.HttpContext.Request.Headers[MyHeaderToken];
}
userSSO = "TestUser";
if (string.IsNullOrWhiteSpace(userSSO))
{
//filterContext.Result = new unh();
}
else
{
string identityType = "My";
string[] methods = new string[0]; // GetMethods(userSSO);
// Create GenericPrincipal
GenericPrincipal principal = this.CreateGenericPrincipal(userSSO, identityType, methods);
context.HttpContext.User = principal;
if (context.HttpContext.Session.IsAvailable)
{
SessionUser su = new SessionUser()
{
IdentityName = principal.Identity.Name,
Type = principal.Identity.AuthenticationType,
Roles = methods
};
byte[] _sessionUser = this.ObjectToByteArray(su);
context.HttpContext.Session.Set("_SessionUser", _sessionUser);
}
}
}
}
}
private GenericPrincipal CreateGenericPrincipal(string name, string type, string[] roles)
{
GenericIdentity webIdentity = new GenericIdentity(name, type);
GenericPrincipal principal = new GenericPrincipal(webIdentity, roles);
return principal;
}
// Convert an object to a byte array
private byte[] ObjectToByteArray(Object obj)
{
BinaryFormatter bf = new BinaryFormatter();
using (var ms = new MemoryStream())
{
bf.Serialize(ms, obj);
return ms.ToArray();
}
}
// Convert a byte array to an Object
private Object ByteArrayToObject(byte[] arrBytes)
{
using (var memStream = new MemoryStream())
{
var binForm = new BinaryFormatter();
memStream.Write(arrBytes, 0, arrBytes.Length);
memStream.Seek(0, SeekOrigin.Begin);
var obj = binForm.Deserialize(memStream);
return obj;
}
}
[Serializable]
private class SessionUser
{
public string IdentityName { get; set; }
public string Type { get; set; }
public string[] Roles { get; set; }
}
}
public class MyAuthorizationAttribute : TypeFilterAttribute
{
public MyAuthorizationAttribute()
: base(typeof(MyAuthorizationFilter))
{
}
}
On Startup.cs > Configure call app.UseSession(); immediately after app.UseRouting() so that session will be available during authorization.
The code above will set the current HTTP Context's user and save it on session. Subsequent requests will attempt to use the user stored on the session. This will also make the DI container manage the lifetime of the filter. Read more about it in Filters in ASP.NET Core.
I do not recommend you follow this approach. Please do either cookie or token-based authentication by taking advantage of the authentication middleware in .NET Core.
Once the request reaches the action execution, context.HttpContext.User.Identity.IsAuthenticated will now be true.

IAuthorizationFilter and IOperationFilter with swagger

Summary
What I want is to be able to add a custom AuthorizeAttribute to a method which is then recognised by swagger and displays the Available authorizations popup. The issue I'm having is getting the IOperationFilter to work correctly with the IAuthorizationFilter.
Code
startup.cs
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v2", new OpenApiInfo { Title = "API", Version = "v2" });
// Adds "(Auth)" to the summary so that you can see which endpoints have Authorization
c.OperationFilter<AppendAuthorizeToSummaryOperationFilter<Filters.Authorization2>>();
c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
Name = "Bearer",
Description = "Standard Authorization header using the SessionKey scheme. Example: \"{token}\"",
In = ParameterLocation.Header,
Type = SecuritySchemeType.ApiKey
});
c.OperationFilter<Filters.AuthorizeCheckOperationFilter>();
});
controller.cs
[Filters.Authorization2]
[HttpGet]
public ApiResult<List<Request>> GetRequest(int ID)
Authorization2.cs
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class Authorization2 : AuthorizeAttribute, IAuthorizationFilter
{
public Authorization2()
{
}
public void OnAuthorization(AuthorizationFilterContext context)
{
//Get the session from the cache
Session sess = GetSession(context.HttpContext);
if (sess.IsValid)
{
//set sess
}
else
{
//If it's not there then return with bad news
context.Result = Unauthorized();
context.HttpContext.Response.StatusCode = 401;
}
}
}
AuthorizeCheckOperationFilter.cs
internal class AuthorizeCheckOperationFilter : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
context.ApiDescription.TryGetMethodInfo(out var methodInfo);
if (methodInfo == null)
return;
var hasAuthorizeAttribute = false;
if (methodInfo.MemberType == MemberTypes.Method)
{
// NOTE: Check the controller itself has Authorize attribute
hasAuthorizeAttribute = methodInfo.DeclaringType.GetCustomAttributes(true).OfType<Authorization2>().Any();
// NOTE: Controller has Authorize attribute, so check the endpoint itself.
// Take into account the allow anonymous attribute
if (hasAuthorizeAttribute)
hasAuthorizeAttribute = !methodInfo.GetCustomAttributes(true).OfType<AllowAnonymousAttribute>().Any();
else
hasAuthorizeAttribute = methodInfo.GetCustomAttributes(true).OfType<Authorization2>().Any();
}
if (!hasAuthorizeAttribute)
return;
if (!operation.Responses.Any(r => r.Key == StatusCodes.Status401Unauthorized.ToString()))
operation.Responses.Add(StatusCodes.Status401Unauthorized.ToString(), new OpenApiResponse { Description = "Unauthorized" });
if (!operation.Responses.Any(r => r.Key == StatusCodes.Status403Forbidden.ToString()))
operation.Responses.Add(StatusCodes.Status403Forbidden.ToString(), new OpenApiResponse { Description = "Forbidden" });
// NOTE: This adds the "Padlock" icon to the endpoint in swagger,
// we can also pass through the names of the policies in the string[]
// which will indicate which permission you require.
operation.Security = new List<OpenApiSecurityRequirement>
{
new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
},
Scheme = "oauth2",
Name = "Bearer",
In = ParameterLocation.Header
},
new List<string>()
}
}
};
}
}
With the code as it is, the padlock on the end point shows up and the header is set but the method throws an 500 error:
System.InvalidOperationException: No authenticationScheme was specified, and there was no DefaultChallengeScheme found.
Attempt 1
I tried adding:
services.AddAuthentication(Microsoft.AspNetCore.Server.IISIntegration.IISDefaults.AuthenticationScheme);
but I get the same error. If I try:
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme,
options =>
{
options.LoginPath = new PathString("/auth/login");
options.AccessDeniedPath = new PathString("/auth/denied");
});
My request redirects to the loginPath, which causes a 404.
Attempt 2
If try a different tacktick and use a TypeFilterAttribute as so:
modify startup.cs
c.AddSecurityRequirement(new OpenApiSecurityRequirement()
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
},
Scheme = "oauth2",
Name = "Bearer",
In = ParameterLocation.Header,
},
new List<string>()
}
});
//c.OperationFilter<Filters.AuthorizeCheckOperationFilter>();
Authorization.cs
public class AuthorizationHandler : IAuthorizationFilter
{
public AuthorizationHandler()
{
}
public void OnAuthorization(AuthorizationFilterContext context)
{
//Get the session from the cache
Session sess = GetSession(context.HttpContext);
if (sess.IsValid)
{
//set sess
}
else
{
//If it's not there then return with bad news
context.Result = Unauthorized();
context.HttpContext.Response.StatusCode = 401;
}
}
}
public class AuthorizeAttribute : TypeFilterAttribute
{
public AuthorizeAttribute() : base(typeof(AuthorizationHandler))
{
}
}
and update the method to use [Filters.Authorize] the call to the method works as expected but now every method get's the lock, not just the ones with the attribute.
Question
How do I modify my code to only have locks on the methods with the attribute and process the authorization correctly?
After following this response to a different question:
https://stackoverflow.com/a/61365691/2885471
I managed to get it to work. In essence I created a security definition in the SwaggerGen service configuration, put the security requirement in a IOperationFilter class and then add the the class to the OperationFilters.

Building an integration test for an AspNetCore API that uses IdentityServer 4 for Auth

I've built a simple AspNetCore 2.2 API that uses IdentityServer 4 to handle OAuth. It's working fine but I'd now like to add integration tests and recently discovered this. I used it to build some tests which all worked fine - as long as I didn't have the [Authorize] attribute on my controllers - but obviously that attribute needs to be there.
I came across this stackoverflow question and from the answers given there I tried to put a test together but I'm still getting an Unauthorized response when I try to run tests.
Please note: I really don't know what details I should be using when I'm creating the client.
What should the allowed scopes be? (Should they match the real
scopes)
Also when building the IdentityServerWebHostBuilder
What should I pass to .AddApiResources? (Maybe a dumb question but
does it matter)
If anyone can guide me it would be greatly appreciated.
Here is my test:
[Fact]
public async Task Attempt_To_Test_InMemory_IdentityServer()
{
// Create a client
var clientConfiguration = new ClientConfiguration("MyClient", "MySecret");
var client = new Client
{
ClientId = clientConfiguration.Id,
ClientSecrets = new List<Secret>
{
new Secret(clientConfiguration.Secret.Sha256())
},
AllowedScopes = new[] { "api1" },
AllowedGrantTypes = new[] { GrantType.ClientCredentials },
AccessTokenType = AccessTokenType.Jwt,
AllowOfflineAccess = true
};
var webHostBuilder = new IdentityServerWebHostBuilder()
.AddClients(client)
.AddApiResources(new ApiResource("api1", "api1name"))
.CreateWebHostBuilder();
var identityServerProxy = new IdentityServerProxy(webHostBuilder);
var tokenResponse = await identityServerProxy.GetClientAccessTokenAsync(clientConfiguration, "api1");
// *****
// Note: creating an IdentityServerProxy above in order to get an access token
// causes the next line to throw an exception stating: WebHostBuilder allows creation only of a single instance of WebHost
// *****
// Create an auth server from the IdentityServerWebHostBuilder
HttpMessageHandler handler;
try
{
var fakeAuthServer = new TestServer(webHostBuilder);
handler = fakeAuthServer.CreateHandler();
}
catch (Exception e)
{
throw;
}
// Create an auth server from the IdentityServerWebHostBuilder
HttpMessageHandler handler;
try
{
var fakeAuthServer = new TestServer(webHostBuilder);
handler = fakeAuthServer.CreateHandler();
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
// Set the BackChannelHandler of the 'production' IdentityServer to use the
// handler form the fakeAuthServer
Startup.BackChannelHandler = handler;
// Create the apiServer
var apiServer = new TestServer(new WebHostBuilder().UseStartup<Startup>());
var apiClient = apiServer.CreateClient();
apiClient.SetBearerToken(tokenResponse.AccessToken);
var user = new User
{
Username = "simonlomax#ekm.com",
Password = "Password-123"
};
var req = new HttpRequestMessage(new HttpMethod("GET"), "/api/users/login")
{
Content = new StringContent(JsonConvert.SerializeObject(user), Encoding.UTF8, "application/json"),
};
// Act
var response = await apiClient.SendAsync(req);
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
My Startup class:
public class Startup
{
public IConfiguration Configuration { get; }
public static HttpMessageHandler BackChannelHandler { get; set; }
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
ConfigureAuth(services);
services.AddTransient<IPassportService, PassportService>();
services.Configure<ApiBehaviorOptions>(options =>
{
options.SuppressModelStateInvalidFilter = true;
});
}
protected virtual void ConfigureAuth(IServiceCollection services)
{
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = Configuration.GetValue<string>("IdentityServerAuthority");
options.Audience = Configuration.GetValue<string>("IdentityServerAudience");
options.BackchannelHttpHandler = BackChannelHandler;
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseHsts();
}
app.UseAuthentication();
app.UseHttpsRedirection();
app.UseMvc();
app.UseExceptionMiddleware();
}
}
Edit:
The below suggestion was one problem. The original source-code failed due to an exception by trying to build WebHostBuilder twice. Secondly the configuration-file was only present in the API project, not in the test-project, thats why authority wasn't set as well.
Instead of doing this
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = Configuration.GetValue<string>("IdentityServerAuthority");
options.Audience = Configuration.GetValue<string>("IdentityServerAudience");
options.BackchannelHttpHandler = BackChannelHandler;
});
You have to do something like this:
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddIdentityServerAuthentication(options =>
{
options.Authority = Configuration.GetValue<string>("IdentityServerAuthority");
options.JwtBackChannelHandler = BackChannelHandler;
});
You can find a sample here.
Hope that helps, worked for me!
A solution which doesn't affect production code:
public class TestApiWebApplicationFactory<TStartup>
: WebApplicationFactory<TStartup> where TStartup : class
{
private readonly HttpClient _identityServerClient;
public TestApiWebApplicationFactory(HttpClient identityServerClient)
{
_identityServerClient = identityServerClient;
}
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
base.ConfigureWebHost(builder);
builder.ConfigureServices(
s =>
{
s.AddSingleton<IConfigureOptions<JwtBearerOptions>>(services =>
{
return new TestJwtBearerOptions(_identityServerClient);
});
});
}
}
and its usage is:
_factory = new WebApplicationFactory<Startup>()
{
ClientOptions = {BaseAddress = new Uri("http://localhost:5000/")}
};
_apiFactory = new TestApiWebApplicationFactory<SampleApi.Startup>(_factory.CreateClient())
{
ClientOptions = {BaseAddress = new Uri("http://localhost:5001/")}
};
The TestJwtBearerOptions just proxies requests to identityServerClient. The implementation you can find here:
https://gist.github.com/ru-sh/048e155d73263912297f1de1539a2687
If you don't want to rely on a static variable to hold the HttpHandler, I've found the following to work. I think it's a lot cleaner.
First create an object that you can instantiate before your TestHost is created. This is because you won't have the HttpHandler until after the TestHost is created, so you need to use a wrapper.
public class TestHttpMessageHandler : DelegatingHandler
{
private ILogger _logger;
public TestHttpMessageHandler(ILogger logger)
{
_logger = logger;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
_logger.Information($"Sending HTTP message using TestHttpMessageHandler. Uri: '{request.RequestUri.ToString()}'");
if (WrappedMessageHandler == null) throw new Exception("You must set WrappedMessageHandler before TestHttpMessageHandler can be used.");
var method = typeof(HttpMessageHandler).GetMethod("SendAsync", BindingFlags.Instance | BindingFlags.NonPublic);
var result = method.Invoke(this.WrappedMessageHandler, new object[] { request, cancellationToken });
return await (Task<HttpResponseMessage>)result;
}
public HttpMessageHandler WrappedMessageHandler { get; set; }
}
Then
var testMessageHandler = new TestHttpMessageHandler(logger);
var webHostBuilder = new WebHostBuilder()
...
services.PostConfigureAll<JwtBearerOptions>(options =>
{
options.Audience = "http://localhost";
options.Authority = "http://localhost";
options.BackchannelHttpHandler = testMessageHandler;
});
...
var server = new TestServer(webHostBuilder);
var innerHttpMessageHandler = server.CreateHandler();
testMessageHandler.WrappedMessageHandler = innerHttpMessageHandler;

ASP.NET Core Web API Authentication

I'm struggling with how to set up authentication in my web service.
The service is build with the ASP.NET Core web api.
All my clients (WPF applications) should use the same credentials to call the web service operations.
After some research, I came up with basic authentication - sending a username and password in the header of the HTTP request.
But after hours of research, it seems to me that basic authentication is not the way to go in ASP.NET Core.
Most of the resources I found are implementing authentication using OAuth or some other middleware. But that seems to be oversized for my scenario, as well as using the Identity part of ASP.NET Core.
So what is the right way to achieve my goal - simple authentication with username and password in a ASP.NET Core web service?
Now, after I was pointed in the right direction, here's my complete solution:
This is the middleware class which is executed on every incoming request and checks if the request has the correct credentials. If no credentials are present or if they are wrong, the service responds with a 401 Unauthorized error immediately.
public class AuthenticationMiddleware
{
private readonly RequestDelegate _next;
public AuthenticationMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context)
{
string authHeader = context.Request.Headers["Authorization"];
if (authHeader != null && authHeader.StartsWith("Basic"))
{
//Extract credentials
string encodedUsernamePassword = authHeader.Substring("Basic ".Length).Trim();
Encoding encoding = Encoding.GetEncoding("iso-8859-1");
string usernamePassword = encoding.GetString(Convert.FromBase64String(encodedUsernamePassword));
int seperatorIndex = usernamePassword.IndexOf(':');
var username = usernamePassword.Substring(0, seperatorIndex);
var password = usernamePassword.Substring(seperatorIndex + 1);
if(username == "test" && password == "test" )
{
await _next.Invoke(context);
}
else
{
context.Response.StatusCode = 401; //Unauthorized
return;
}
}
else
{
// no authorization header
context.Response.StatusCode = 401; //Unauthorized
return;
}
}
}
The middleware extension needs to be called in the Configure method of the service Startup class
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
app.UseMiddleware<AuthenticationMiddleware>();
app.UseMvc();
}
And that's all! :)
A very good resource for middleware in .Net Core and authentication can be found here:
https://www.exceptionnotfound.net/writing-custom-middleware-in-asp-net-core-1-0/
You can implement a middleware which handles Basic authentication.
public async Task Invoke(HttpContext context)
{
var authHeader = context.Request.Headers.Get("Authorization");
if (authHeader != null && authHeader.StartsWith("basic", StringComparison.OrdinalIgnoreCase))
{
var token = authHeader.Substring("Basic ".Length).Trim();
System.Console.WriteLine(token);
var credentialstring = Encoding.UTF8.GetString(Convert.FromBase64String(token));
var credentials = credentialstring.Split(':');
if(credentials[0] == "admin" && credentials[1] == "admin")
{
var claims = new[] { new Claim("name", credentials[0]), new Claim(ClaimTypes.Role, "Admin") };
var identity = new ClaimsIdentity(claims, "Basic");
context.User = new ClaimsPrincipal(identity);
}
}
else
{
context.Response.StatusCode = 401;
context.Response.Headers.Set("WWW-Authenticate", "Basic realm=\"dotnetthoughts.net\"");
}
await _next(context);
}
This code is written in a beta version of asp.net core. Hope it helps.
To use this only for specific controllers for example use this:
app.UseWhen(x => (x.Request.Path.StartsWithSegments("/api", StringComparison.OrdinalIgnoreCase)),
builder =>
{
builder.UseMiddleware<AuthenticationMiddleware>();
});
I think you can go with JWT (Json Web Tokens).
First you need to install the package System.IdentityModel.Tokens.Jwt:
$ dotnet add package System.IdentityModel.Tokens.Jwt
You will need to add a controller for token generation and authentication like this one:
public class TokenController : Controller
{
[Route("/token")]
[HttpPost]
public IActionResult Create(string username, string password)
{
if (IsValidUserAndPasswordCombination(username, password))
return new ObjectResult(GenerateToken(username));
return BadRequest();
}
private bool IsValidUserAndPasswordCombination(string username, string password)
{
return !string.IsNullOrEmpty(username) && username == password;
}
private string GenerateToken(string username)
{
var claims = new Claim[]
{
new Claim(ClaimTypes.Name, username),
new Claim(JwtRegisteredClaimNames.Nbf, new DateTimeOffset(DateTime.Now).ToUnixTimeSeconds().ToString()),
new Claim(JwtRegisteredClaimNames.Exp, new DateTimeOffset(DateTime.Now.AddDays(1)).ToUnixTimeSeconds().ToString()),
};
var token = new JwtSecurityToken(
new JwtHeader(new SigningCredentials(
new SymmetricSecurityKey(Encoding.UTF8.GetBytes("Secret Key You Devise")),
SecurityAlgorithms.HmacSha256)),
new JwtPayload(claims));
return new JwtSecurityTokenHandler().WriteToken(token);
}
}
After that update Startup.cs class to look like below:
namespace WebAPISecurity
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddAuthentication(options => {
options.DefaultAuthenticateScheme = "JwtBearer";
options.DefaultChallengeScheme = "JwtBearer";
})
.AddJwtBearer("JwtBearer", jwtBearerOptions =>
{
jwtBearerOptions.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("Secret Key You Devise")),
ValidateIssuer = false,
//ValidIssuer = "The name of the issuer",
ValidateAudience = false,
//ValidAudience = "The name of the audience",
ValidateLifetime = true, //validate the expiration and not before values in the token
ClockSkew = TimeSpan.FromMinutes(5) //5 minute tolerance for the expiration date
};
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseAuthentication();
app.UseMvc();
}
}
And that's it, what is left now is to put [Authorize] attribute on the Controllers or Actions you want.
Here is a link of a complete straight forward tutorial.
http://www.blinkingcaret.com/2017/09/06/secure-web-api-in-asp-net-core/
I have implemented BasicAuthenticationHandler for basic authentication so you can use it with standart attributes Authorize and AllowAnonymous.
public class BasicAuthenticationHandler : AuthenticationHandler<BasicAuthenticationOptions>
{
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
var authHeader = (string)this.Request.Headers["Authorization"];
if (!string.IsNullOrEmpty(authHeader) && authHeader.StartsWith("basic", StringComparison.OrdinalIgnoreCase))
{
//Extract credentials
string encodedUsernamePassword = authHeader.Substring("Basic ".Length).Trim();
Encoding encoding = Encoding.GetEncoding("iso-8859-1");
string usernamePassword = encoding.GetString(Convert.FromBase64String(encodedUsernamePassword));
int seperatorIndex = usernamePassword.IndexOf(':', StringComparison.OrdinalIgnoreCase);
var username = usernamePassword.Substring(0, seperatorIndex);
var password = usernamePassword.Substring(seperatorIndex + 1);
//you also can use this.Context.Authentication here
if (username == "test" && password == "test")
{
var user = new GenericPrincipal(new GenericIdentity("User"), null);
var ticket = new AuthenticationTicket(user, new AuthenticationProperties(), Options.AuthenticationScheme);
return Task.FromResult(AuthenticateResult.Success(ticket));
}
else
{
return Task.FromResult(AuthenticateResult.Fail("No valid user."));
}
}
this.Response.Headers["WWW-Authenticate"]= "Basic realm=\"yourawesomesite.net\"";
return Task.FromResult(AuthenticateResult.Fail("No credentials."));
}
}
public class BasicAuthenticationMiddleware : AuthenticationMiddleware<BasicAuthenticationOptions>
{
public BasicAuthenticationMiddleware(
RequestDelegate next,
IOptions<BasicAuthenticationOptions> options,
ILoggerFactory loggerFactory,
UrlEncoder encoder)
: base(next, options, loggerFactory, encoder)
{
}
protected override AuthenticationHandler<BasicAuthenticationOptions> CreateHandler()
{
return new BasicAuthenticationHandler();
}
}
public class BasicAuthenticationOptions : AuthenticationOptions
{
public BasicAuthenticationOptions()
{
AuthenticationScheme = "Basic";
AutomaticAuthenticate = true;
}
}
Registration at Startup.cs - app.UseMiddleware<BasicAuthenticationMiddleware>();. With this code, you can restrict any controller with standart attribute Autorize:
[Authorize(ActiveAuthenticationSchemes = "Basic")]
[Route("api/[controller]")]
public class ValuesController : Controller
and use attribute AllowAnonymous if you apply authorize filter on application level.
As rightly said by previous posts, one of way is to implement a custom basic authentication middleware. I found the best working code with explanation in this blog:
Basic Auth with custom middleware
I referred the same blog but had to do 2 adaptations:
While adding the middleware in startup file -> Configure function, always add custom middleware before adding app.UseMvc().
While reading the username, password from appsettings.json file, add static read only property in Startup file. Then read from appsettings.json. Finally, read the values from anywhere in the project. Example:
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public static string UserNameFromAppSettings { get; private set; }
public static string PasswordFromAppSettings { get; private set; }
//set username and password from appsettings.json
UserNameFromAppSettings = Configuration.GetSection("BasicAuth").GetSection("UserName").Value;
PasswordFromAppSettings = Configuration.GetSection("BasicAuth").GetSection("Password").Value;
}
You can use an ActionFilterAttribute
public class BasicAuthAttribute : ActionFilterAttribute
{
public string BasicRealm { get; set; }
protected NetworkCredential Nc { get; set; }
public BasicAuthAttribute(string user,string pass)
{
this.Nc = new NetworkCredential(user,pass);
}
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var req = filterContext.HttpContext.Request;
var auth = req.Headers["Authorization"].ToString();
if (!String.IsNullOrEmpty(auth))
{
var cred = System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(auth.Substring(6)))
.Split(':');
var user = new {Name = cred[0], Pass = cred[1]};
if (user.Name == Nc.UserName && user.Pass == Nc.Password) return;
}
filterContext.HttpContext.Response.Headers.Add("WWW-Authenticate",
String.Format("Basic realm=\"{0}\"", BasicRealm ?? "Ryadel"));
filterContext.Result = new UnauthorizedResult();
}
}
and add the attribute to your controller
[BasicAuth("USR", "MyPassword")]
In this public Github repo
https://github.com/boskjoett/BasicAuthWebApi
you can see a simple example of a ASP.NET Core 2.2 web API with endpoints protected by Basic Authentication.
ASP.NET Core 2.0 with Angular
https://fullstackmark.com/post/13/jwt-authentication-with-aspnet-core-2-web-api-angular-5-net-core-identity-and-facebook-login
Make sure to use type of authentication filter
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]

Categories

Resources