I have enabled basic authentication in a web api:
// configure basic authentication
builder.Services.AddAuthentication("BasicAuthentication")
.AddScheme<AuthenticationSchemeOptions, BasicAuthenticationHandler>("BasicAuthentication", null);
builder.Services.AddAuthorization();
BasicAuthenticationHandler:
public class BasicAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
private readonly IUserRepositoryService _userRepo;
public BasicAuthenticationHandler(
IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock,
IUserRepositoryService userRepo
) : base(options, logger, encoder, clock)
{
_userRepo = userRepo;
}
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
var authHeader = Request.Headers["Authorization"].ToString();
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 (await _userRepo.ValidateCredentialsAsync(credentials[0], credentials[1]))
{
// var uid = _userRepo.GetUserIdAsync(credentials[0], credentials[1]);
var claims = new[] {
new Claim(ClaimTypes.Name, credentials[0]),
//new Claim(ClaimTypes.NameIdentifier, uid)
};
var identity = new ClaimsIdentity(claims, "Basic");
var claimsPrincipal = new ClaimsPrincipal(identity);
return AuthenticateResult.Success(new AuthenticationTicket(claimsPrincipal, Scheme.Name));
}
}
Response.StatusCode = 401;
Response.Headers.Add("WWW-Authenticate", "Basic realm=\"API\"");
return AuthenticateResult.Fail("Invalid Authorization Header");
}
}
This works globally as expected but when trying to use [AllowAnonymous] it doesn't work. The login prompt always pops-up from the BasicAuthenticationHandler. I'm not sure how to detect the attribute when inside the AuthenticateResult.
Should I create a custom [BasicAuth] attribute to enforce authentication instead?
Related
While following Microsoft's ASP.NET Core guide for integration testing authentication, I have the following test built for Authentication:
[Fact]
public async Task Get_SecurePageIsReturnedForAnAuthenticatedUser()
{
// Arrange
var client = _factory.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
services.AddAuthentication("Test")
.AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(
"Test", options => {});
});
})
.CreateClient(new WebApplicationFactoryClientOptions
{
AllowAutoRedirect = false,
});
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Test");
//Act
var response = await client.GetAsync("/SecurePage");
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
What I want to do, is use the [Theory] option instead of [Fact] to test multiple Authentications so it will look like this:
[Theory]
[InlineData("TestAuth1","12345")]
[InlineData("TestAuth2","23456")]
[InlineData("TestAuth3","34567")]
public async Task Get_SecurePageIsReturnedForAnAuthenticatedUser(string claim, string claimsIdentity)
{
var claim = new Claim(claim, claimsIdentity);
.
.
.
However I'm not sure how to pass claim to TestAuthHandler through AddScheme<AuthenticationSchemeOptions, TestAuthHandler>
Here is the given TestAuthHandler
public class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
public TestAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
: base(options, logger, encoder, clock)
{
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
var claims = new[] { new Claim(ClaimTypes.Name, "Test user") };
var identity = new ClaimsIdentity(claims, "Test");
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, "Test");
var result = AuthenticateResult.Success(ticket);
return Task.FromResult(result);
}
}
I would like to replace the claims variable in HandleAuthenticaAsync() with the claim passed into Get_SecurePageIsReturnedForAnAuthenticatedUser(Claim claim)
As a note they do have to be tested individually since my current authentication will pass as long as one correct authentication exists in the HandleAuthenticateAsync claims variable.
Thank you for any help provided.
I had a similar problem and was able to solve it by creating a custom TestAuthenticationSchemeOptions which implements AuthenticationSchemeOptions which would have a Claims property similar to the example in this link below
How to claim role for HttpContext.User.IsInRole method check in integration test?
e.g.
you would have
public class TestAuthenticationSchemeOptions : AuthenticationSchemeOptions
{
public IEnumerable<Claim> Claims { get; set; }
}
In your test
// Arrange
var inputClaims = new List<Claim> { new Claim(ClaimTypes.Name, "Test user") };
var client = _factory.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
services.AddAuthentication("Test")
.AddScheme<TestAuthenticationSchemeOptions, TestAuthHandler>(
"Test", options => { options.Claims = inputClaims; });
});
})
then in your AuthHandler you can grab the claims using Options.Claims
public class TestAuthHandler : AuthenticationHandler<TestAuthenticationSchemeOptions>
{
public TestAuthHandler(IOptionsMonitor<TestAuthenticationSchemeOptions> options,
ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
: base(options, logger, encoder, clock)
{
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
var claims = Options.Claims;
var identity = new ClaimsIdentity(claims, "Test");
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, "Test");
var result = AuthenticateResult.Success(ticket);
return Task.FromResult(result);
}
}
I have an ASP.Net Web API 2 with BasicAuthenticationAttribute that is working as expected. In my application, there are different controllers and I want to add bearer token-based authentication to one of my controllers. I added those NuGet packages:
Microsoft.AspNet.WebApi.Owin
Microsoft.Owin.Host.SystemWeb
Microsoft.Owin.Security.OAuth
Here is the WebApiConfig:
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.Formatters.JsonFormatter.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
config.Formatters.JsonFormatter.SerializerSettings.DateTimeZoneHandling =
DateTimeZoneHandling.Local;
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
"DefaultApi",
"api/{controller}/{id}",
new {id = RouteParameter.Optional}
);
config.MessageHandlers.Add(new RequestResponseHandler());
config.Filters.Add(new CustomExceptionFilter());
var resolver = config.DependencyResolver; //Assuming one is set.
var basicAuth = (BasicAuthenticationAttribute)resolver.GetService(typeof(BasicAuthenticationAttribute));
// Web API configuration and services
if (basicAuth != null) config.Filters.Add(basicAuth);
}
}
Here is the Owin Startup
public class Startup
{
public void Configuration(IAppBuilder app)
{
var configuration = GlobalConfiguration.Configuration;
WebApiConfig.Register(configuration);
app.UseWebApi(configuration);
Configure(app);
}
private static void Configure(IAppBuilder app)
{
var options = new OAuthAuthorizationServerOptions()
{
TokenEndpointPath = new Microsoft.Owin.PathString("/token"),
AccessTokenExpireTimeSpan = TimeSpan.FromHours(1),
AllowInsecureHttp = true,
Provider = new AuthorizationServerProvider()
};
app.UseOAuthAuthorizationServer(options);
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());
}
}
Here is the controller
[RoutePrefix("api/v2/game/abc101")]
public class A101Controller : ApiController
{
private readonly IGameServicesABC101 _gameServices;
private readonly IMapper _mapper;
public A101Controller(IGameServicesABC101 gameServices, IMapper mapper)
{
_gameServices = gameServices;
_mapper = mapper;
}
[HttpPost]
[Authorize]
[Route("purchase")]
public async Task<IHttpActionResult> PurchaseGame(RequestDto game)
{
if (!ModelState.IsValid) return BadRequest(ModelState);
...
Basic Authentication Attribute
public class BasicAuthenticationAttribute : AuthorizationFilterAttribute
{
private const string Realm = "My Realm";
private readonly Func<IUserValidate> _factory;
public BasicAuthenticationAttribute(Func<IUserValidate> factory)
{
_factory = factory;
}
public override void OnAuthorization(HttpActionContext actionContext)
{
if (actionContext.Request.Headers.Authorization == null)
{
actionContext.Response = actionContext.Request
.CreateResponse(HttpStatusCode.Unauthorized);
if (actionContext.Response.StatusCode == HttpStatusCode.Unauthorized)
actionContext.Response.Headers.Add("WWW-Authenticate",
$"Basic realm=\"{Realm}\"");
}
else
{
var authenticationToken = actionContext.Request.Headers
.Authorization.Parameter;
try
{
//Decode the string
var decodedAuthenticationToken = Encoding.UTF8.GetString(
Convert.FromBase64String(authenticationToken));
var usernamePasswordArray = decodedAuthenticationToken.Split(':');
var username = usernamePasswordArray[0];
var password = usernamePasswordArray[1];
var uv = _factory();
if (uv.Login(username, password))
{
var identity = new GenericIdentity(username);
IPrincipal principal = new GenericPrincipal(identity, null);
Thread.CurrentPrincipal = principal;
if (HttpContext.Current != null) HttpContext.Current.User = principal;
}
else
{
actionContext.Response = actionContext.Request
.CreateResponse(HttpStatusCode.Unauthorized);
}
}
catch
{
actionContext.Response = actionContext.Request
.CreateResponse(HttpStatusCode.Unauthorized);
}
}
}
}
I am using Unity in my application. Basic Authentication works as expected. When I make a request without a token to ...api/v2/game/abc101/purchase I get a response either. Shouldn't I get 401? What I am missing?
UPDATE
I am searching and trying to find how to use both basic authentication and token-based authentication for different controllers. Here is my status update.
There is no code in the Global.asax
Here is the Owin Startup
public class Startup
{
public void Configuration(IAppBuilder app)
{
var config = GlobalConfiguration.Configuration;
WebApiConfig.Register(config);
app.UseWebApi(config);
Configure(app, config.DependencyResolver);
config.EnsureInitialized();
}
private static void Configure(IAppBuilder app, IDependencyResolver resolver)
{
var options = new OAuthAuthorizationServerOptions()
{
TokenEndpointPath = new Microsoft.Owin.PathString("/token"),
AccessTokenExpireTimeSpan = TimeSpan.FromHours(1),
AllowInsecureHttp = true,
Provider = new AuthorizationServerProvider((IUserValidate)resolver.GetService(typeof(IUserValidate)))
};
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());
app.UseOAuthAuthorizationServer(options);
}
}
Here is AuthorizationServerProvider
public class AuthorizationServerProvider : OAuthAuthorizationServerProvider
{
private readonly IUserValidate _userValidate;
public AuthorizationServerProvider(IUserValidate userValidate)
{
_userValidate = userValidate;
}
public override async Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
{
if (!context.TryGetBasicCredentials(out var clientId, out var clientSecret))
{
context.SetError("Error", "Error...");
}
if (_userValidate.Login(clientId, clientSecret))
{
context.Validated();
}
}
public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] { "*" });
context.OwinContext.Response.Headers.Add("Access-Control-Allow-Headers", new[] { "Content-Type" });
if (_userValidate.Login(context.UserName, context.Password))
{
var identity = new ClaimsIdentity(context.Options.AuthenticationType);
identity.AddClaim(new Claim("sub", context.UserName));
identity.AddClaim(new Claim("role", "admin"));
context.Validated(identity);
}
else
{
context.SetError("Error", "Error...");
}
}
}
The rest is the same as the previous code samples.
When I call ...api/v2/game/abc101/purchase I am getting 401, it is progress. But when I call http://localhost:52908/token I am getting unsupported_grant_type. I am sending requests via Postman and I am sending a POST requests with content-type x-www-form-urlencoded. Grant-Type is password and username/password is also correct.
When I call another controller http://localhost:52908/api/v2/game/purchase basic authentication does NOT work!
Hope someone can help.
UPDATE 1
Now I am getting the token, one step at a time :) How can I also use Basic authentication for another controller?
Here is Startup
public class Startup
{
public void Configuration(IAppBuilder app)
{
var config = GlobalConfiguration.Configuration;
Configure(app, config.DependencyResolver);
WebApiConfig.Register(config);
app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);
app.UseWebApi(config);
config.EnsureInitialized();
}
private static void Configure(IAppBuilder app, IDependencyResolver resolver)
{
var options = new OAuthAuthorizationServerOptions()
{
AllowInsecureHttp = true,
TokenEndpointPath = new Microsoft.Owin.PathString("/token"),
AccessTokenExpireTimeSpan = TimeSpan.FromHours(1),
Provider = new AuthorizationServerProvider((IUserValidate)resolver.GetService(typeof(IUserValidate)))
};
app.UseOAuthAuthorizationServer(options);
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());
}
}
Here is the Authorization Server Provider
public class AuthorizationServerProvider : OAuthAuthorizationServerProvider
{
private readonly IUserValidate _userValidate;
public AuthorizationServerProvider(IUserValidate userValidate)
{
_userValidate = userValidate;
}
public override async Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
{
if (!context.TryGetBasicCredentials(out var clientId, out var clientSecret))
{
context.SetError("invalid_grant", "The user name or password is incorrect.");
}
if (_userValidate.Login(clientId, clientSecret))
{
context.Validated();
}
}
public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
if (_userValidate.Login(context.UserName, context.Password))
{
var identity = new ClaimsIdentity(context.Options.AuthenticationType);
identity.AddClaim(new Claim("sub", context.UserName));
identity.AddClaim(new Claim("role", "admin"));
context.Validated(identity);
}
else
{
context.SetError("invalid_grant", "The user name or password is incorrect.");
}
}
}
As I mentioned before, I have Basic Authentication Attribute and somehow I have to use it in my other controller.
UPDATE 2
How can I use OverrideAuthentication and my basic authentication attribute?
public class BasicAuthenticationAttribute : AuthorizationFilterAttribute
{
private const string Realm = "My Realm";
private readonly Func<IUserValidate> _factory;
public BasicAuthenticationAttribute(Func<IUserValidate> factory)
{
_factory = factory;
}
...
UPDATE 3
I tried this in my basic authentication attribute OnAuthorization method;
var authentication = DependencyResolver.Current.GetService<IUserValidate>();
if (authentication.Login(username, password))
{
var identity = new GenericIdentity(username);
IPrincipal principal = new GenericPrincipal(identity, null);
Thread.CurrentPrincipal = principal;
if (HttpContext.Current != null) HttpContext.Current.User = principal;
}
There are 2 problems, authentication is null, and somehow authentication token in the attribute is the bearer authentication username/password even though I use basic authentication username/password in the request. It's very weird!
/Get the authentication token from the request header
var authenticationToken = actionContext.Request.Headers
.Authorization.Parameter;
Any help please?
Thanks in advance.
After long googling, here is how I managed to use both basic authentication and bearer authentication for my different controllers.
In Custom basic authentication Attribute I used dependency and requestScope.GetService.
public class CustomBasicAuthenticationAttribute : AuthorizationFilterAttribute
{
[Dependency] public static IUserValidate authentication { get; set; }
private const string Realm = "My Realm";
public override void OnAuthorization(HttpActionContext actionContext)
{
var requestScope = actionContext.Request.GetDependencyScope();
//If the Authorization header is empty or null
//then return Unauthorized
if (actionContext.Request.Headers.Authorization == null)
{
actionContext.Response = actionContext.Request
.CreateResponse(HttpStatusCode.Unauthorized);
// If the request was unauthorized, add the WWW-Authenticate header
// to the response which indicates that it require basic authentication
if (actionContext.Response.StatusCode == HttpStatusCode.Unauthorized)
actionContext.Response.Headers.Add("WWW-Authenticate",
$"Basic realm=\"{Realm}\"");
}
else
{
//Get the authentication token from the request header
var authenticationToken = actionContext.Request.Headers
.Authorization.Parameter;
try
{
//Decode the string
var decodedAuthenticationToken = Encoding.UTF8.GetString(
Convert.FromBase64String(authenticationToken));
//Convert the string into an string array
var usernamePasswordArray = decodedAuthenticationToken.Split(':');
//First element of the array is the username
var username = usernamePasswordArray[0];
//Second element of the array is the password
var password = usernamePasswordArray[1];
authentication = requestScope.GetService(typeof(IUserValidate)) as IUserValidate;
if (authentication != null && authentication.Login(username, password))
{
var identity = new GenericIdentity(username);
IPrincipal principal = new GenericPrincipal(identity, null);
Thread.CurrentPrincipal = principal;
if (HttpContext.Current != null) HttpContext.Current.User = principal;
}
else
{
actionContext.Response = actionContext.Request
.CreateResponse(HttpStatusCode.Unauthorized);
}
}
catch
{
actionContext.Response = actionContext.Request
.CreateResponse(HttpStatusCode.Unauthorized);
}
}
}
}
In one of my controller, I added those attributes
[OverrideAuthentication]
[CustomBasicAuthentication]
[HttpPost, Route("purchase")]
public async Task<IHttpActionResult> PurchaseGame(RequestDto game)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
...
Now I can use bearer token authentication for ...api/v2/game/abc101/purchase and basic authentication for ...api/v2/game/purchase.
Update
The vital part is the dependency and actionContext.Request.GetDependencyScope();. Without OverrideAuthentication it is working as expected.
Hope this solution helps for others.
So on the SignIn Method
public IActionResult SignIn()
{
if (_unitOfWork.User.IsAuth(HttpContext) == true)
{
var _userCurrentObject = _unitOfWork.User.GetCurrentUserObject(HttpContext);
var claims = new List<Claim>
{
new Claim("UserType", _userCurrentObject.UserType),
};
var appIdentity = new ClaimsIdentity(claims);
HttpContext.User.AddIdentity(appIdentity);
User.AddIdentity(appIdentity);
User.Claims.Append(new Claim("Wow", "value-x"));
var zz = User;// i can see the Claim which i Add here but in other Action not able to see those Claims
return RedirectToAction("Index", "Home");
}
else
{
return RedirectToAction("AccessDenied", "Home");
}
}
Tying to access those claims in other controllers like this
string UserType = User.Claims.FirstOrDefault(c => c.Type == "UserType")?.Value;
Till here everything working fine but when I app trying to access user in other action not able to see custom claims which I add Like "Usertype"
Am I missing something?
You can save User claims and use them using DI in an elegant way using IMemoryCache. The code goes like below:
Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<IPrincipal>(
provider => provider.GetService<IHttpContextAccessor>().HttpContext.User);
services.AddTransient<IClaimsTransformation, ClaimsTransformer>();
services.AddAuthentication(IISDefaults.AuthenticationScheme);
}
ClaimsTransformer.cs:
using Microsoft.Extensions.Caching.Memory;
public class ClaimsTransformer : IClaimsTransformation
{
private readonly IMemoryCache _cache;
public ClaimsTransformer(IMemoryCache cache)
{
_cache = cache;
}
public async Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
{
var cacheKey = principal.FindFirstValue(ClaimTypes.NameIdentifier);
if (_cache.TryGetValue(cacheKey, out List<Claim> claims)
{
((ClaimsIdentity)principal.Identity).AddClaims(claims);
}
else
{
claims = new List<Claim>();
// call to database to get more claims based on user id ClaimsIdentity.Name
_cache.Set(cacheKey, claims);
}
return principal;
}
}
I would also recommend you to read about IMemoryCache here: Cache in-memory in ASP.NET Core
public class ClaimsTransformer : IClaimsTransformation
{
private readonly IUnitOfWork _unitOfWork;
public const string SessionKeyByPassUserName = "_LoggedIn_ByPassUserName";
//public ClaimsTransformer(D2CContext context)
public ClaimsTransformer(IUnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork;
}
public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
{
var identity = principal.Identities.FirstOrDefault(x => x.IsAuthenticated);
if (identity == null) return Task.FromResult(principal);
var userEmail = identity?.Claims.FirstOrDefault(c => c.Type == "preferred_username")?.Value;
var userObj = _unitOfWork.TblUserSecurity.GetEmail(userEmail);
var domainUserName = userObj?.UserName;
//checking if bypass user cookie exists, setting it to domainUserName
IHttpContextAccessor _accessor = new HttpContextAccessor();
//if (_accessor.HttpContext.Request.Cookies["bypassUser"] != null)
//{
// domainUserName = _accessor.HttpContext.Request.Cookies["bypassUser"];
//}
if (string.IsNullOrEmpty(domainUserName)) return Task.FromResult(principal);
var existingClaims = identity.Claims;
var claims = new List<Claim>();
claims.AddRange(existingClaims);
if (string.IsNullOrWhiteSpace(identity.FindFirst("LoggedInUserName")?.Value))
claims.Add(new Claim("LoggedInUserName", userObj.UserType));
claims.Add(new Claim("UserType", userObj.UserType ));
claims.Add(new Claim("IsViewCaseManager", userObj.IsViewCaseManager != null ? userObj.IsViewCaseManager.ToString() : "False"));
claims.Add(new Claim("IsDefaultCaseManager", userObj.IsDefaultCaseManager != null ? userObj.IsDefaultCaseManager.ToString() : "False"));
claims.Add(new Claim("UserName", userObj.UserName));
// if (string.IsNullOrWhiteSpace(identity.FindFirst(ClaimTypes.Role)?.Value))
// claims.Add(new Claim(ClaimTypes.Role, userObj.Role.RoleName));
var newClaimsIdentity = new ClaimsIdentity(claims, "Kerberos", "", "http://schemas.microsoft.com/ws/2008/06/identity/claims/role");
var newClaimsPrincipal = new ClaimsPrincipal(newClaimsIdentity);
return Task.FromResult(new ClaimsPrincipal(newClaimsPrincipal));
}
}
}
Middleware
Service.AddTrasnsient<IClaimsTransformation,ClaimsTransformer >();
I a have a very simple app with one JWT authenticated controller:
[ApiController]
[Authorize]
[Route("[controller]")]
public class JwtController : ControllerBase
{
public JwtController() { }
[HttpGet]
public ActionResult Get() => Ok("Working!");
}
With the authentication configured as:
services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(x =>
{
x.RequireHttpsMetadata = false;
x.SaveToken = true;
x.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false,
ValidateAudience = false
};
});
During tests, i want the user to be "authenticated" all the time so that [Authorize] would be skipped.
[Fact]
public async Task JwtIsSkipped()
{
var response = (await _Client.GetAsync("/jwt")).EnsureSuccessStatusCode();
var stringResponse = await response.Content.ReadAsStringAsync();
Assert.Equal("Working!", stringResponse);
}
Running the test like this will fail, so following this doc I added this simple auth handler:
public class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
public const string DefaultScheme = "Test";
public TestAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
: base(options, logger, encoder, clock)
{
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
var claims = new[] { new Claim(ClaimTypes.Name, "Test user") };
var identity = new ClaimsIdentity(claims, DefaultScheme);
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, DefaultScheme);
return Task.FromResult(AuthenticateResult.Success(ticket));
}
}
So now my test class looks like this:
public class UnitTest : IClassFixture<WebApplicationFactory<Startup>>
{
private readonly WebApplicationFactory<Startup> _Factory;
private readonly HttpClient _Client;
public UnitTest(WebApplicationFactory<Startup> factory)
{
_Factory = factory;
_Client = _Factory.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
services.AddAuthentication(TestAuthHandler.DefaultScheme)
.AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(
TestAuthHandler.DefaultScheme, options => { });
});
}).CreateClient();
_Client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(TestAuthHandler.DefaultScheme);
}
[Fact]
public async Task JwtIsSkipped()
{
var response = (await _Client.GetAsync("/jwt")).EnsureSuccessStatusCode();
var stringResponse = await response.Content.ReadAsStringAsync();
Assert.Equal("Working!", stringResponse);
}
}
And it still fails, I have no idea what I'm doing wrong.
I have had a similar situation previously with the Microsoft example and can promise you it can give headaches, it may work on specific Core versions, but I have given up. I have solved this way.
My goal was, is to Authorize the system while testing, instead of using AddAuthentication in our test we create a FakePolicyEvaluator class and add it as a singleton to our test.
So let's go to our FakePolicyEvaluator class:
public class FakePolicyEvaluator : IPolicyEvaluator
{
public virtual async Task<AuthenticateResult> AuthenticateAsync(AuthorizationPolicy policy, HttpContext context)
{
var principal = new ClaimsPrincipal();
principal.AddIdentity(new ClaimsIdentity(new[] {
new Claim("Permission", "CanViewPage"),
new Claim("Manager", "yes"),
new Claim(ClaimTypes.Role, "Administrator"),
new Claim(ClaimTypes.NameIdentifier, "John")
}, "FakeScheme"));
return await Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(principal,
new AuthenticationProperties(), "FakeScheme")));
}
public virtual async Task<PolicyAuthorizationResult> AuthorizeAsync(AuthorizationPolicy policy,
AuthenticateResult authenticationResult, HttpContext context, object resource)
{
return await Task.FromResult(PolicyAuthorizationResult.Success());
}
}
Then in our ConfigureTestServices we added services.AddSingleton<IPolicyEvaluator, FakePolicyEvaluator>();
So in your test code like this:
private readonly HttpClient _client;
public UnitTest(WebApplicationFactory<Startup> factory)
{
_client = factory.WithWebHostBuilder(builder =>
{
builder.ConfigureTestServices(services =>
{
services.AddSingleton<IPolicyEvaluator, FakePolicyEvaluator>();
});
}).CreateClient();
}
[Fact]
public async Task JwtIsSkipped()
{
var response = (await _client.GetAsync("/jwt")).EnsureSuccessStatusCode();
var stringResponse = await response.Content.ReadAsStringAsync();
Assert.Equal("Working!", stringResponse);
}
That is it. Now when you test, it will bypass authentication. I have tested it with the provided controller and it works.
It is also possible to place the fake inside the application startup, and it will be both testable for test and working under a development environment. Check the referenced article.
Disclaimer: I have written in more depth article about this on my personal website Reference where you can find and download a source code from GitHub.
You need to set DefaultAuthenticateScheme
builder.ConfigureTestServices(services =>
{
services.AddAuthentication(options =>
{
x.DefaultAuthenticateScheme = TestAuthHandler.DefaultScheme;
x.DefaultScheme = TestAuthHandler.DefaultScheme;
}).AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(
TestAuthHandler.DefaultScheme, options => { });
});
its a small change to maysam fahmi answer that HttpContext.User also have values:
public class FakeUserPolicyEvaluator: IPolicyEvaluator
{
private ClaimsIdentity _claimsIdentity;
public virtual async Task<AuthenticateResult> AuthenticateAsync(AuthorizationPolicy policy, HttpContext context)
{
var testScheme = "FakeScheme";
var principal = new ClaimsPrincipal();
_claimsIdentity = new ClaimsIdentity(new[]
{
new Claim("sub", "a5"),
new Claim("client_id", "a6"),
new Claim(ClaimTypes.Role, BackmanConsts.Authorization.ClientPolicy),
}, testScheme);
principal.AddIdentity(_claimsIdentity);
return await Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(principal,
new AuthenticationProperties(), testScheme)));
}
public virtual async Task<PolicyAuthorizationResult> AuthorizeAsync(AuthorizationPolicy policy,
AuthenticateResult authenticationResult, HttpContext context, object resource)
{
context.User = new ClaimsPrincipal(_claimsIdentity);
return await Task.FromResult(PolicyAuthorizationResult.Success());
}
}
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)]