I need to add a Basic Authentication to a set of API.
DISCLAIMER: The APIs are inside an intranet and contains public data. They can be consumed from external users only through an API gateway where strong authentication and authorization are implemented. This Basic Authentication is needed only to avoid that internal development team calls directly this service. User and password are not the real one.
I've used the package ZNetCS.AspNetCore.Authentication.Basic, version 4.0.0
This is my startup.cs:
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public void ConfigureServices(IServiceCollection services)
{
services
.AddAuthentication(BasicAuthenticationDefaults.AuthenticationScheme)
.AddBasicAuthentication(
options =>
{
options.Realm = "My Application";
options.Events = new BasicAuthenticationEvents
{
OnValidatePrincipal = context =>
{
if ((context.UserName.ToLower() == "gateway0107")
&& (context.Password == "jir6STt6437yMAQpl"))
{
var claims = new List<Claim>{
new Claim(ClaimTypes.Name,
context.UserName,
context.Options.ClaimsIssuer)
};
var ticket = new AuthenticationTicket(
new ClaimsPrincipal(new ClaimsIdentity(
claims,
BasicAuthenticationDefaults.AuthenticationScheme)),
new Microsoft.AspNetCore.Authentication.AuthenticationProperties(),
BasicAuthenticationDefaults.AuthenticationScheme);
return Task.FromResult(AuthenticateResult.Success(ticket));
}
return Task.FromResult(AuthenticateResult.Fail("Authentication failed."));
}
};
});
services.AddMvcCore();
services.AddApiVersioning(
options =>
{
options.ReportApiVersions = true;
});
services.AddVersionedApiExplorer(
options =>
{
options.GroupNameFormat = "'v'VVV";
options.SubstituteApiVersionInUrl = true;
});
services.AddTransient<IConfigureOptions<SwaggerGenOptions>, ConfigureSwaggerOptions>();
services.AddSwaggerGen(
options =>
{
options.OperationFilter<SwaggerDefaultValues>();
options.IncludeXmlComments(XmlCommentsFilePath);
});
services.AddCors(options =>
{
options.AddPolicy(name: "AllowAllOrigins",
builder =>
{
builder.AllowAnyOrigin();
});
});
services.AddControllers(options =>
{
options.ModelMetadataDetailsProviders.Add(new SuppressChildValidationMetadataProvider(typeof(Point)));
options.ModelMetadataDetailsProviders.Add(new SuppressChildValidationMetadataProvider(typeof(Coordinate)));
options.ModelMetadataDetailsProviders.Add(new SuppressChildValidationMetadataProvider(typeof(LineString)));
options.ModelMetadataDetailsProviders.Add(new SuppressChildValidationMetadataProvider(typeof(MultiLineString)));
}).AddNewtonsoftJson(options =>
{
foreach (var converter in NetTopologySuite.IO.GeoJsonSerializer.Create(new GeometryFactory(new PrecisionModel(), 4326)).Converters)
{
options.SerializerSettings.NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore;
options.SerializerSettings.Converters.Add(converter);
}
}).SetCompatibilityVersion(CompatibilityVersion.Version_3_0);
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IApiVersionDescriptionProvider provider)
{
app.UseForwardedHeaders();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(
Path.Combine(Directory.GetCurrentDirectory(), "swagger-ui")),
RequestPath = "/swagger-ui"
});
app.UseHttpsRedirection();
app.UseRouting();
app.UseCors("AllowAllOrigins");
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
app.UseSwagger();
app.UseSwaggerUI(
options =>
{
// build a swagger endpoint for each discovered API version
foreach (var description in provider.ApiVersionDescriptions)
{
options.SwaggerEndpoint($"/swagger/{description.GroupName}/swagger.json", description.GroupName.ToUpperInvariant());
}
});
}
static string XmlCommentsFilePath
{
get
{
var basePath = PlatformServices.Default.Application.ApplicationBasePath;
var fileName = typeof(Startup).GetTypeInfo().Assembly.GetName().Name + ".xml";
return Path.Combine(basePath, fileName);
}
}
}
But testing in Postman and from SwaggerUI I get always 410 Unauthorized.
Setting a breakpoint inside the OnValidatePrincipal, it never gets hit.
Should I have to add something else?
Related
I have been trying to figure this out all weekend, but can't seem to get it to work. Please help.
I have setup IdentityServer 4, an MVC application and an API.
I get an access token after login, but when I try to access the API it throws 500 internal server error (if I have an Authorize attribute)
this is my identityserver config file:
public static IEnumerable<IdentityResource> IdentityResources =>
new IdentityResource[]
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
};
public static IEnumerable<ApiScope> ApiScopes =>
new ApiScope[]
{
new ApiScope("api1", "my API")
};
public static IEnumerable<Client> Clients =>
new Client[]
{
new Client
{
ClientId = "mvc",
ClientSecrets = { new Secret("secret".Sha256()) },
AllowedGrantTypes = GrantTypes.Code,
RedirectUris = { "https://localhost:5002/signin-oidc" },
//FrontChannelLogoutUri = "https://localhost:5002/signout-oidc",
PostLogoutRedirectUris = { "https://localhost:5002/signout-callback-oidc" },
AllowOfflineAccess = true,
AllowedScopes = new List<string>
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
"api1"
}
}
};
This is the identityServer startup class:
public IWebHostEnvironment Environment { get; }
public IConfiguration Configuration { get; }
public Startup(IWebHostEnvironment environment, IConfiguration configuration)
{
Environment = environment;
Configuration = configuration;
}
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
if (System.Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == "Production")
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("MyDbConnection")));
else
services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddIdentity<ApplicationUser, IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
var builder = services.AddIdentityServer(options =>
{
options.Events.RaiseErrorEvents = true;
options.Events.RaiseInformationEvents = true;
options.Events.RaiseFailureEvents = true;
options.Events.RaiseSuccessEvents = true;
// see https://identityserver4.readthedocs.io/en/latest/topics/resources.html
options.EmitStaticAudienceClaim = true;
})
.AddInMemoryIdentityResources(Config.IdentityResources)
.AddInMemoryApiScopes(Config.ApiScopes)
.AddInMemoryClients(Config.Clients)
.AddAspNetIdentity<ApplicationUser>();
// not recommended for production - you need to store your key material somewhere secure
builder.AddDeveloperSigningCredential();
services.AddAuthentication()
.AddGoogle(options =>
{
options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
options.ClientId = "xxx.apps.googleusercontent.com";
options.ClientSecret = "xxx";
options.ReturnUrlParameter = "https://xxx.azurewebsites.net/signin-google";
}).AddFacebook(facebookOptions =>
{
facebookOptions.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme;
facebookOptions.ClientId = "xxx";
facebookOptions.ClientSecret = "xxx";
facebookOptions.ReturnUrlParameter = "https://xxx.azurewebsites.net/signin-facebook";
});
}
public void Configure(IApplicationBuilder app)
{
if (Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
app.UseStaticFiles();
app.UseRouting();
app.UseIdentityServer();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapDefaultControllerRoute();
});
}
This is my API startup class:
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;
using System.Configuration;
namespace SF.API
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration
{
get;
}
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
// accepts any access token issued by identity server
services.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", options =>
{
options.Authority = ConfigurationManager.AppSettings["IdentityServerAddress"];
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false
};
});
// adds an authorization policy to make sure the token is for scope 'api1'
//services.AddAuthorization(options =>
//{
// options.AddPolicy("ApiScope", policy =>
// {
// policy.RequireAuthenticatedUser();
// policy.RequireClaim("scope", "api1");
// });
//});
}
public void Configure(IApplicationBuilder app)
{
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute("default", "{controller=Home}/{action=Index}/{id?}");
});
app.UseStaticFiles();
}
}
}
this is the Controller class:
[Route("identity")]
[Authorize]
public class IdentityController : ControllerBase
{
public IActionResult Get()
{
return new JsonResult(from c in User.Claims select new { c.Type, c.Value });
}
[Route("/Test")]
public IActionResult Test()
{
return new JsonResult(from c in User.Claims select new { c.Type, c.Value });
}
}
This is the MVC application startup class:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
JwtSecurityTokenHandler.DefaultMapInboundClaims = false;
services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies")
.AddOpenIdConnect("oidc", options =>
{
options.Authority = "https://xxx.azurewebsites.net";
options.ClientId = "mvc";
options.ClientSecret = "secret";
options.ResponseType = "code";
options.SaveTokens = true;
options.Scope.Add("api1");
options.Scope.Add("offline_access");
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapDefaultControllerRoute()
.RequireAuthorization();
});
}
This is how I call the API:
public async Task<IActionResult> CallApi()
{
var accessToken = await HttpContext.GetTokenAsync("access_token");
var client = new HttpClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
var content = await client.GetStringAsync("https://localhost:6001/Test");
ViewBag.Json = JArray.Parse(content).ToString();
return View("json");
}
My access token looks like this:
"eyJhbGciOiJSUzI1NiIsImtpZCI6IkExNDYxOUUzOTAwNjM5ODA2NUU4RkUwQjJFMkU1RThFIiwidHlwIjoiYXQrand0In0.eyJuYmYiOjE2NDg5ODEyMjMsImV4cCI6MTY0ODk4NDgyMywiaXNzIjoiaHR0cHM6Ly9pZGVudGl0eXNlcnZlcmlkZW50aXR5cHJvdmlkZXJzZi5henVyZXdlYnNpdGVzLm5ldCIsImF1ZCI6Imh0dHBzOi8vaWRlbnRpdHlzZXJ2ZXJpZGVudGl0eXByb3ZpZGVyc2YuYXp1cmV3ZWJzaXRlcy5uZXQvcmVzb3VyY2VzIiwiY2xpZW50X2lkIjoibXZjIiwic3ViIjoiZDc5NGQxOGUtNGYyOC00MmE3LWFkMTQtZDdiMWMxMDcwOTE5IiwiYXV0aF90aW1lIjoxNjQ4OTgxMjIzLCJpZHAiOiJsb2NhbCIsImp0aSI6IjZBRTE3Q0RBMjBGQkNGNDExQzc3QUIyQkNBNTE3M0YzIiwic2lkIjoiRkMxREY5QUZCODQyRkZEN0JGRjk5MTY0RTYyN0M2ODYiLCJpYXQiOjE2NDg5ODEyMjMsInNjb3BlIjpbIm9wZW5pZCIsInByb2ZpbGUiLCJhcGkxIiwib2ZmbGluZV9hY2Nlc3MiXSwiYW1yIjpbInB3ZCJdfQ.Grcu-dbrLy6LHHxsW2FJsSIhmwQBEl1jQ2LRvJhBFzZ8j0HAqk129Q8JncJFSFBjQkEls8xBFN-OxyvhJ5o7dmgpkgYENbfjl7jC04yhvSh_MzLqG2h_mme1mwsC3xzuKbQR1yczei-j92WUMeP-CvzUtr2vbJd2lJv0YvpJvykGF4BbKrQMPLPZnlLFRkPm5LcdFfUsrHrCz3R0JZ7tpVSwGMjGMlDHlAMAR04Fzf6YQhbKUEydNdTIWFP2akyBoWuRwAvTXbOA8vm9GZpeTbo8S4At5X7RhOR_J-zIjk1QWKhqN9kVMnMLXpO_NmZ6iQ66pcnT0G75rtFEfFtISQ"
Is there anything wrong with how I access the token? I can't see the scopes for example?
What else could be wrong?
**Update: I added app.UseAuthorization() in the API.Startup class. Then I got 401 Unauthroized instead
One problem with the API is that it lacks
app.UseAuthentication();
app.UseAuthorization();
In the API startup class.
To debug the API in ASP.NET Core, you can try to set the log level top debug or trace, like this in appsetttings:
{
"Logging": {
"LogLevel": {
"Default": "Warning",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information",
"Microsoft.AspNetCore.Authentication": "Trace",
"Microsoft.AspNetCore.Authorization": "Trace"
}
}
}
You should check inside the access token header:
{
"alg": "RS256",
"kid": "A14619E39006398065E8FE0B2E2E5E8E",
"typ": "at+jwt"
}
The kid key identifier must be present in the JWKS endpoint.
following problem I can create a token but when I use it the authentication failed, I try to copy from an example but I had problems because
some methods are not "there". (like principal.SetScopes, but it seems to be exist in the Github repository and in other examples)
The only error I get is the failure of the AuthorizationFilter.
Here the method for creating the token
[HttpPost("~/connect/token"), Produces("application/json")]
public async Task<IActionResult> Exchange(OpenIdConnectRequest connectRequest)
{
if (connectRequest.IsPasswordGrantType())
{
var user = await _userManager.FindByNameAsync(connectRequest.Username);
if (user == null)
{
return Forbid(
authenticationSchemes: OpenIddictServerDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIdConnectConstants.Properties.Error] = OpenIddictConstants.Errors.InvalidGrant,
[OpenIdConnectConstants.Properties.ErrorDescription] = "The username/password couple is invalid."
}));
}
var result = await _signInManager.CheckPasswordSignInAsync(user, connectRequest.Password, lockoutOnFailure: true);
if (!result.Succeeded)
{
return Forbid(
authenticationSchemes: OpenIddictValidationDefaults.AuthenticationScheme,
properties: new AuthenticationProperties(new Dictionary<string, string>
{
[OpenIdConnectConstants.Properties.Error] = Errors.InvalidGrant,
[OpenIdConnectConstants.Properties.ErrorDescription] = "The username/password couple is invalid."
}));
}
var principal = await _signInManager.CreateUserPrincipalAsync(user);
//principal.SetScopes(new[]
//{
// Scopes.OpenId,
// Scopes.Email,
// Scopes.Profile,
// Scopes.Roles
//}.Intersect(connectRequest.GetScopes()));
//foreach (var claim in principal.Claims)
//{
// claim.SetDestinations(GetDestinations(claim, principal));
//}
var sign = SignIn(principal, OpenIddictServerDefaults.AuthenticationScheme);
return sign;
}
throw new Exception("Not supported");
}
Here is my startup
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<DocumentiveContext>((provider, builder) =>
{
var configuration = provider.GetService<IConfiguration>();
builder.UseSqlServer(configuration.GetConnectionString("connectionString"));
builder.UseOpenIddict();
});
services.AddIdentity<ApplicationUser, IdentityRole>(options =>
{
})
.AddEntityFrameworkStores<DocumentiveContext>()
.AddDefaultTokenProviders();
services.ConfigureApplicationCookie(options =>
{
options.Events.OnRedirectToAccessDenied = context =>
{
context.Response.StatusCode = 401;
return Task.CompletedTask;
};
options.Events.OnRedirectToLogin = context =>
{
context.Response.StatusCode = 401;
return Task.CompletedTask;
};
});
services.Configure<IdentityOptions>(options =>
{
options.ClaimsIdentity.UserNameClaimType = Claims.Name;
options.ClaimsIdentity.UserIdClaimType = Claims.Subject;
options.ClaimsIdentity.RoleClaimType = Claims.Role;
});
services.AddAuthentication(options =>
options.DefaultScheme = OpenIddictValidationDefaults.AuthenticationScheme);
services.AddOpenIddict(builder =>
{
builder.AddCore(coreBuilder => coreBuilder.UseEntityFrameworkCore().UseDbContext<DocumentiveContext>());
builder.AddServer(serverBuilder =>
{
serverBuilder.UseMvc();
//serverBuilder.EnableTokenEndpoint("/connect/token");
serverBuilder.EnableAuthorizationEndpoint("/connect/authorize")
.EnableLogoutEndpoint("/connect/logout")
.EnableTokenEndpoint("/connect/token")
.EnableUserinfoEndpoint("/connect/userinfo")
.EnableUserinfoEndpoint("/connect/verify");
serverBuilder.AllowPasswordFlow();
serverBuilder.RegisterScopes(Scopes.Email, Scopes.Profile, Scopes.Roles, "demo_api");
serverBuilder.AddDevelopmentSigningCertificate();
serverBuilder.AcceptAnonymousClients();
serverBuilder.DisableHttpsRequirement();
});
builder.AddValidation(validationBuilder =>
{
});
});
services.AddMvc(options => options.EnableEndpointRouting = false);
services.AddGrpc();
services.AddTransient<IUserInformationService, UserInformationService>();
}
// 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.UseRouting();
app.UseAuthentication();
app.UseStaticFiles();
app.UseSerilogRequestLogging();
app.UseAuthorization();
app.UseMvcWithDefaultRoute();
app.UseEndpoints(endpoints =>
{
endpoints.MapGrpcService<GreeterService>();
endpoints.MapGet("/", async context =>
{
await context.Response.WriteAsync("Communication with gRPC endpoints must be made through a gRPC client. To learn how to create a client, visit: https://go.microsoft.com/fwlink/?linkid=2086909");
});
});
}
}
Any idea?
After updating to the newest Version 3 of Openiddict, it works.
Also I needed to set the Authentication Scheme in the attribute.
[Authorize(AuthenticationSchemes = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme)]
Unable to Identify the error after publishing the file
at System.Text.Encoding.GetBytes(String s)
at Inspection.Apis.Startup.ConfigureServices(IServiceCollection services) in C:\Users\me_mo\source\InspectionBackend\Inspection.Apis\Startup.cs:line 66
Line 66 is Swagger configuration
I tried to run also on release mode and its running fine.
Finding out this error just on deployment
namespace Inspection.Apis
{
public class Startup
{
public Startup(IHostingEnvironment environment, IConfiguration configuration)
{
Environment = environment;
Configuration = configuration;
}
public IHostingEnvironment Environment { get; }
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.AddAutoMapper(typeof(AutoMapperMappings).Assembly);
services.AddDbContext<InspectionEfContext, InspectionContext>();
services.AddDbContext<InspectionContext>();
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
services.AddHttpContextAccessor();
services.AddCors(options =>
{
options.AddPolicy("AllowAll",
builder =>
{
builder
.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials();
});
});
EntityFrameworkManager.ContextFactory = context =>
{
return new InspectionContext((context as InspectionContext).RequestInfo);
};
services.AddSwaggerGen(options =>
{
options.SwaggerDoc("v1", new OpenApiInfo { Title = "Inspection Suite API", Version = "v1" });
options.OperationFilter<TenantParameterOperationFilter>();
options.AddSecurityDefinition("Bearer",
new OpenApiSecurityScheme
{
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\"",
Name = "Authorization",
In = ParameterLocation.Header,
Type = SecuritySchemeType.ApiKey,
Scheme = "Bearer"
});
options.AddSecurityRequirement(new OpenApiSecurityRequirement {
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
},
Scheme = "oauth2",
Name = "Bearer",
In = ParameterLocation.Header,
},
new List<string>() }
});
});
#region JWT
var key = Encoding.ASCII.GetBytes(Configuration["JwtKey"]);
services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(x =>
{
x.RequireHttpsMetadata = false;
x.SaveToken = true;
x.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
ValidateIssuer = false,
ValidateAudience = false
};
});
#endregion
services.AddMvc().AddJsonOptions(options =>
{
options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
if (Environment.IsProduction())
{
options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore;
options.SerializerSettings.DefaultValueHandling = DefaultValueHandling.Ignore;
}
});
services.AddScoped<IRequestInfo>(provider =>
{
var context = provider.GetRequiredService<IHttpContextAccessor>();
var claims = context.HttpContext.User.Claims;
return new RequestInfo(this.Configuration, int.TryParse(claims.FirstOrDefault(o => o.Type == "tenantId")?.Value, out int tenantId) ? (int?)tenantId : null);
});
services.AddScoped<RequestScope<InspectionContext>>(provider =>
{
var dbContext = provider.GetRequiredService<InspectionContext>();
var scope = provider.GetRequiredService<RequestScope>();
var userId = provider.GetRequiredService<IHttpContextAccessor>().HttpContext.User.FindFirst(x => x.Type == "UserId")?.Value;
return new RequestScope<InspectionContext>(scope.ServiceProvider, dbContext, scope.Logger, scope.Mapper, userId, scope.TenantId);
});
services.AddScoped<RequestScope<InspectionEfContext>>(provider =>
{
var dbContext = provider.GetRequiredService<InspectionEfContext>();
var scope = provider.GetRequiredService<RequestScope>();
var userId = provider.GetRequiredService<IHttpContextAccessor>().HttpContext.User.FindFirst(x => x.Type == "UserId")?. Value;
return new RequestScope<InspectionEfContext>(scope.ServiceProvider, dbContext, scope.Logger, scope.Mapper, userId, scope.TenantId);
});
services.AddScoped<RequestScope>(provider =>
{
var logger = provider.GetRequiredService<ILogger<Program>>();
var context = provider.GetRequiredService<IHttpContextAccessor>();
var claims = context.HttpContext.User.Claims;
var userId = claims.FirstOrDefault(o => o.Type == "UserId")?.Value;
var tenantId = int.TryParse(claims.FirstOrDefault(o => o.Type == "tenantId")?.Value, out int t) ? t : default(int?);
if (!tenantId.HasValue)
{
tenantId = int.TryParse(context.HttpContext.Request.Headers["tenantId"].SingleOrDefault(), out t) ? t : default(int?);
}
var mapper = provider.GetRequiredService<IMapper>();
return new RequestScope(provider, logger, mapper, userId, tenantId);
});
ConfigureRepositories(services);
ConfigureAppServices(services);
}
private void ConfigureRepositories(IServiceCollection services)
{
services.AddScoped<IUserRepository, UserRepository>();
services.AddScoped<IFormBuilderTypeRepository, FormBuilderTypeRepository>();
services.AddScoped<IFormBuilderRepository, FormBuilderRepository>();
services.AddScoped<IFormBuilderQuestionsRepository, FormBuilderQuestionsRepository>();
services.AddScoped<IFormBuilderQuestionsResponseRepository, FormBuilderQuestionsResponseRepository>();
}
private void ConfigureAppServices(IServiceCollection services)
{
services.AddScoped<IUserService, UserServices>();
services.AddScoped<IAuthenticationService, AuthenticationService>();
services.AddScoped<UserIdGenerator>();
services.AddScoped<IFormBuilderTypeServices, FormBuilderTypeServices>();
services.AddScoped<IFormBuilderServices, FormBuilderServices>();
services.AddScoped<IFormBuilderQuestionsServices, FormBuilderQuestionsServices>();
services.AddScoped<IFormBuilderQuestionsResponseServices, FormBuilderQuestionsResponseServices>();
}
// 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
{
// 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.UseMiddleware<InspectionExceptionMiddleware>();
app.UseHttpsRedirection();
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
});
app.UseCors("AllowAll");
app.UseAuthentication();
app.UseHttpsRedirection();
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "Inspection API V1");
c.RoutePrefix = string.Empty;
});
app.UseMvc();
}
}
public class TenantParameterOperationFilter : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
if (operation.Parameters == null)
operation.Parameters = new List<OpenApiParameter>();
operation.Parameters.Add(new OpenApiParameter
{
Name = "tenantId",
In = ParameterLocation.Header,
Description = "Tenant Id"
});
}
}
}
//need to do initialization value in ---> appsettings.json <---- folder
// i did this
{
"JWT": {
"ServerSecret": "qwertyuiopasdfghjklzxcvbnm123456",
"Issuer": "https://awesome.io",
"Audience": "https://app.awesome.io"
},
"Logging": {
"LogLevel": {
"Default": "Warning"
}
},
"AllowedHosts": "*"
}
After Yegor Androsov views i reviewed and find out he was right
Configuration[JwtKey] was null due to it was not placed in appsetting.json
it was placed in appsetting.development.json
Also make sure when referencing the key and value in appsetting.json,
use Configuration["JwtKey:Secret"]
User has logged out from the Site but APIs are accessible from POSTMAN with cookies in the header?
Logout
public async Task OnPost(string returnUrl = null)
{
await _httpContextAccessor.HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme,
new AuthenticationProperties
{
RedirectUri = returnUrl
});
await _signInManager.SignOutAsync();
HttpContext.Response.Cookies.Delete(".AspNetCore.Cookies");
}
Startup
public class Startup
{
public Startup(IConfiguration configuration, IHostingEnvironment env)
{
Configuration = configuration;
HostingEnvironment = env;
}
public IConfiguration Configuration { get; }
public IHostingEnvironment HostingEnvironment { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
//Https
services.AddHsts(options =>
{
options.Preload = true;
options.IncludeSubDomains = true;
options.MaxAge = TimeSpan.FromDays(180);
options.ExcludedHosts.Add("admission.just.edu.bd");
options.ExcludedHosts.Add("www.admission.just.edu.bd");
});
services.AddHttpsRedirection(options =>
{
options.RedirectStatusCode = StatusCodes.Status301MovedPermanently;
options.HttpsPort = HostingEnvironment.IsDevelopment() ? 5001 : 443;
});
services.AddMemoryCache();
services.AddDbContext<AdmissionDbContext>(options =>
{
if (HostingEnvironment.IsDevelopment())
{
options.UseSqlServer(Configuration["DbConnection:Sql:Local"], x => x.MigrationsHistoryTable("__EFMigrationsHistory", Configuration["DbConnection:Sql:Schema"]));
}
else
{
options.UseSqlServer(Configuration["DbConnection:Sql:Cloud"], x => x.MigrationsHistoryTable("__EFMigrationsHistory", Configuration["DbConnection:Sql:Schema"]));
}
});
services.AddIdentity<ApplicationUser, IdentityRole>(
options =>
{
options.ClaimsIdentity.UserIdClaimType = ClaimTypes.NameIdentifier;
options.Password.RequireDigit = false;
options.Password.RequiredLength = 6;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireUppercase = false;
options.Password.RequireLowercase = false;
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromHours(2);
})
.AddEntityFrameworkStores<AdmissionDbContext>()
.AddDefaultTokenProviders();
services.AddAuthorization(opts =>
{
opts.AddPolicy("AuthenticatedUser", policy => policy.RequireAuthenticatedUser());
opts.AddPolicy("SystemAdminOnly", policy => policy.RequireRole(SystemRole.Administrator));
opts.AddPolicy("SupportOnly", policy => policy.RequireRole(SystemRole.Support));
opts.AddPolicy("ApplicantOnly", policy => policy.RequireRole(SystemRole.Applicant));
});
services.AddScoped<IClaimsTransformation, ClaimsTransformation>();
services.AddSession();
services.AddMvc(
options =>
{
options.Filters.Add<ErrorExceptionFilter>();
}
).SetCompatibilityVersion(CompatibilityVersion.Version_2_1)
.AddRazorPagesOptions(options =>
{
options.AllowAreas = true;
options.Conventions.AuthorizeAreaFolder("Identity", "/Account/Manage");
options.Conventions.AuthorizeAreaPage("Identity", "/Account/Logout");
})
.AddJsonOptions(opts =>
{
opts.SerializerSettings.NullValueHandling = NullValueHandling.Ignore;
opts.SerializerSettings.DateFormatHandling = DateFormatHandling.IsoDateFormat;
opts.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
opts.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
opts.SerializerSettings.DateTimeZoneHandling = DateTimeZoneHandling.Utc;
if (HostingEnvironment.IsDevelopment())
{
opts.SerializerSettings.Formatting = Formatting.Indented;
}
});
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
//app.UseDeveloperExceptionPage();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseHsts();
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRequestLocalization();
// app.UsePendingMigrations();
app.UseDefaultRoles(SystemRole.All);
app.UseDefaultUsers();
//app.UseCookiePolicy();
app.UseAuthentication();
app.UseSession();
app.UseMvc(routes =>
{
routes.MapRoute("areaRoute", "{area:exists}/{controller}/{action=Index}/{id?}");
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
}
I don't know why, but for some reason Response.Cookies.Delete(cookieKey) was not working for me. What I did was create another cookie with the same cookie name with expiry set to a time in past. Eg:
var c = new HttpCookie("cookieKey");
c.Expires = DateTime.Now.AddDays(-1);
Response.Cookies.Add(c);
I am building a website in dotnet core and have recently started using claims based authentication and authorization.
In a view component I am checking if the user has access to a policy.
public NavigationViewComponent(
IContextManager contextManager,
IAuthorizationService authorizationService)
{
_contextManager = contextManager;
_authorizationService = authorizationService;
}
public async Task<IViewComponentResult> InvokeAsync()
{
var showAdmin = _contextManager.Principal != null &&
(await _authorizationService.AuthorizeAsync(_contextManager.Principal, "Admin")).Succeeded;
var vm = new NavigationViewModel
{
ShowAdmin = showAdmin
};
return View(vm);
}
However, I am receiving the Exception InvalidOperationException: No policy found: Admin..
My startup.cs contains the following inside the ConfigureServices method:
services.AddAuthorization(options =>
{
options.AddPolicy("Admin",
policy => policy.Requirements.Add(new HasPermissionRequirement("ADMIN")));
});
What else do I need to configure in order to get this to work correctly?
For reference I am registering 3 additional IAuthorizationHandler implementations and 1 IAuthorizationPolicyProvider.
Edit
For reference, the whole startup.cs looks something similar to this.
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
{
// Name and policy settings
options.Cookie.Name = "account";
options.Cookie.SameSite = SameSiteMode.Strict;
options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
options.Cookie.HttpOnly = true;
// Sign the user out after 28 days of inactivity
options.SlidingExpiration = true;
options.ExpireTimeSpan = TimeSpan.FromDays(28);
// Challenge actions
options.LoginPath = new PathString("/account/login");
options.ReturnUrlParameter = "returnUrl";
});
services.AddAuthorization(options =>
{
options.AddPolicy("Admin",
policy => policy.Requirements.Add(new HasPermissionRequirement("ADMIN")));
});
services.AddSingleton<IAuthorizationHandler, HasPermissionHandler>();
services.AddSingleton<IAuthorizationHandler, StrategyAuthorizationCrudHandler>();
services.AddSingleton<IAuthorizationHandler, UserAuthorizationCrudHandler>();
services.AddSingleton<IAuthorizationPolicyProvider, HasPermissionPolicyProvider>();
// AddAntiforgery, AddSession,AddDistributedRedisCache and AddDataProtection omitted
services.AddMvc()
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1)
.AddJsonOptions(options =>
{
options.SerializerSettings.ReferenceLoopHandling =
ReferenceLoopHandling.Ignore;
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
app.UseCookiePolicy(new CookiePolicyOptions
{
CheckConsentNeeded = httpContext => false,
MinimumSameSitePolicy = SameSiteMode.Strict,
Secure = CookieSecurePolicy.SameAsRequest
});
app.UseAuthentication();
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
});
app.UseStaticFiles();
var supportedCultures = new[]
{
new CultureInfo("en-GB")
};
app.UseRequestLocalization(new RequestLocalizationOptions
{
DefaultRequestCulture = new RequestCulture(supportedCultures[0]),
SupportedCultures = supportedCultures,
SupportedUICultures = supportedCultures
});
app.UseSession();
app.UseMiddleware<UserMiddleware>();
app.UseMiddleware<LoggingMiddleware>();
app.UseMvc(routes =>
{
routes.MapRoute(
"default",
"{controller=Home}/{action=Index}/{id?}");
});
}
HasPermissionRequirement.cs
public class HasPermissionRequirement : IAuthorizationRequirement
{
public string Permission { get; private set; }
public HasPermissionRequirement(string permission)
{
Permission = permission;
}
}
HasPermissionHandler.cs
public class HasPermissionHandler : AuthorizationHandler<HasPermissionRequirement>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
HasPermissionRequirement requirement)
{
var hasPermission = context.User.HasClaim("Permission", requirement.Permission);
if (hasPermission)
context.Succeed(requirement);
return Task.CompletedTask;
}
}
HasPermissionPolicyProvider.cs
public class HasPermissionPolicyProvider : IAuthorizationPolicyProvider
{
private const string PolicyPrefix = "HasPermission";
public Task<AuthorizationPolicy> GetPolicyAsync(string policyName)
{
if (!policyName.StartsWith(PolicyPrefix, StringComparison.OrdinalIgnoreCase))
return Task.FromResult<AuthorizationPolicy>(null);
var permission = policyName.Substring(PolicyPrefix.Length);
var policy = new AuthorizationPolicyBuilder();
policy.AddRequirements(new HasPermissionRequirement(permission));
return Task.FromResult(policy.Build());
}
public Task<AuthorizationPolicy> GetDefaultPolicyAsync() =>
Task.FromResult(new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build());
}
I've managed to solve this by taking a closer look at the Microsoft documentation after being pointed in the right direction.
https://github.com/aspnet/Docs/blob/master/aspnetcore/security/authorization/iauthorizationpolicyprovider.md#multiple-authorization-policy-providers
When using custom IAuthorizationPolicyProvider implementations, keep in mind that ASP.NET Core only uses one instance of IAuthorizationPolicyProvider. If a custom provider isn't able to provide authorization policies for all policy names, it should fall back to a backup provider.
As a result I changed the implementation of my HasPermissionPolicyProvider to CustomPolicyProvider and the content is below:
public class CustomPolicyProvider : DefaultAuthorizationPolicyProvider
{
private const string PermissionPolicyPrefix = "HasPermission";
public CustomPolicyProvider(IOptions<AuthorizationOptions> options) : base(options)
{
}
public override async Task<AuthorizationPolicy> GetPolicyAsync(string policyName)
{
var policy = await base.GetPolicyAsync(policyName);
if (policy != null) return policy;
if (policyName.StartsWith(PermissionPolicyPrefix, StringComparison.OrdinalIgnoreCase))
{
var permission = policyName.Substring(PermissionPolicyPrefix.Length);
return new AuthorizationPolicyBuilder()
.AddRequirements(new HasPermissionRequirement(permission))
.Build();
}
return null;
}
}
This means that you can only have one PolicyProvider which must handle all your logic. The change is to ensure that it calls the default implementation if you require multiple handler logic.