My application is an ASP.NET Core 1.0 Web API.
How do I test a controller which is decorated with the Authorize attribute?
For example, with this controller and test method:
[TestMethod]
public void GetSomeDataTest()
{
var controller = new MyController();
Assert.AreEqual(controller.GetSomeData(), "Test");
}
[Authorize]
public ActionResult GetSomeData()
{
return this.Content("Test");
}
This is just an example code to make it possible for you guys to answer. I am actually invoking the Controller via a TestServer object.
This has already been asked but the accepted answer doesn't work anymore.
Any suggestions how I could "fake" the users' authenticity?
You could set a claim principle to the current thread
[TestInitialize]
public void Initialize()
{
var claims = new List<Claim>()
{
new Claim(ClaimTypes.Name, "UserName"),
new Claim(ClaimTypes.Role, "Admin")
};
var identity = new ClaimsIdentity(claims, "TestAuth");
var claimsPrincipal = new ClaimsPrincipal(identity);
Thread.CurrentPrincipal = claimsPrincipal;
}
For .NET Core, you could set the user to the controller context
private MyController _ctrl;
[TestInitialize]
public void Initialize()
{
var user = new ClaimsPrincipal(new ClaimsIdentity(new Claim[]
{
new Claim(ClaimTypes.Name, "UserName"),
new Claim(ClaimTypes.Role, "Admin")
}));
_ctrl = new MyController();
_ctrl.ControllerContext = new ControllerContext()
{
HttpContext = new DefaultHttpContext() { User = user }
};
}
[TestMethod]
public void GetSomeDataTest()
{
Assert.AreEqual(_ctrl.GetSomeData(), "Test");
}
Well, you are not actually invoking the controller. Rather, you are running a mock test and thus nothing is happening in the conventional way like the ASP.NET engine handling your request -- request passing through HTTP pipeline (thus authorization module).
So while testing, you should only concentrate on the internal logic of the controller action method instead of that Authorize attribute because, in your unit test method, no authentication / authorization will take place. You will setup mock and call the controller action method like any other method.
Related
I have a method in a controller that check's the user's role to see which page the user should be redirected.
I am trying to do unit testing for the following code
public class HomeController: Controller {
//...
public IActionResult Home()
{
if (User.IsInRole("Administrators")
{
return RedirectToAction("/administrator");
}
else
{
return RedirectToAction("/user");
}
}
//...other actions
}
That mock setup is incorrect; you actually cannot mock members of concrete classes (unless they're virtual). But thankfully that's no big deal here, because you don't need to use a mock. Generally speaking the following is a working way of dealing with HttpContext testing:
[Test]
public async Task HomeReturnsNotNull()
{
var controller = new HomeController();
var controllerCtx = new ControllerContext()
{
HttpContext = new DefaultHttpContext()
{
User = new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.Role, "Administrators") }))
}
};
controller.ControllerContext = controllerCtx;
ActionResult index = (ActionResult)controller.Home();
Assert.IsNotNull(index);
}
But it's often more comfortable to rely on some abstractions of your own, instead of calling HttpContext members directly.
Took the liberty to change 'admin' to 'Administrators', because I noticed a discrepancy between the value in the test and the value in the controller action.
This way the IsInRole() check in the controller action should execute properly, which should be useful in the other tests (since in this one it seems you're just checking against null).
I have created a custom authorization filter in ASP.NET Core 2.2 MVC in order to handle regular and AJAX requests, and to redirect to a custom URL if user is not authorized.
On some of my controller actions, I have the filter set [CustomAuthorize(Roles = "ExampleRole")]. Since I made a custom authorization filter, I thought I would need to also write the logic to check the role claims. However, CustomAuthorize filter is able to correctly handle roles without any additional code.
How is this happening? Is it the additional code inherited from AuthorizeAttribute class that continues to run after the custom OnAuthorization method runs?
Code for custom authorization filter:
public class CustomAuthorize : AuthorizeAttribute, IAuthorizationFilter
{
public void OnAuthorization(AuthorizationFilterContext context)
{
string redirectUrl = "/Auth/Login";
if (context.HttpContext.User.Identity.IsAuthenticated == false)
{
if (context.HttpContext.Request.IsAjaxRequest())
{
context.HttpContext.Response.StatusCode = 401;
//result is returned to AJAX call and user is redirected to sign in page
JsonResult jsonResult = new JsonResult(new { message = "Unauthorized", redirectUrl = redirectUrl });
context.Result = jsonResult;
}
else
{
context.Result = new RedirectResult(redirectUrl);
}
}
}
}
When you inherit AuthorizeAttribute, you will naturally overwrite the IAuthorizeData interface. When the project starts, the attributes that implement the IAuthorizeData interface will be converted into the corresponding filter through AuthorizationApplicationModelProvider. About 2.x, he conversion process can be viewed in this code https://github.com/dotnet/aspnetcore/blob/2.1.3/src/Mvc/Mvc.Core/src/ApplicationModels/AuthorizationApplicationModelProvider.cs.
Then, its authorization rules are passed to the filter.
In a controller in an ASP.NET Core web application I want to refresh the user and claims in the cookie ticket stored on the client.
The client is authenticated and authorized, ASP.NET Core Identity stores this Information in the cookie ticket - now in some Controller actions I want to refresh the data in the cookie.
The SignInManager has a function to refresh RefreshSignInAsync, but it does not accept HttpContext.User as parameter.
[HttpPost("[action]")]
[Authorize]
public async Task<IActionResult> Validate()
{
// todo: update the Client Cookie
await _signInManager.RefreshSignInAsync(User); // wrong type
}
How do I refresh the cookie?
public static class HttpContextExtensions
{
public static async Task RefreshLoginAsync(this HttpContext context)
{
if (context.User == null)
return;
// The example uses base class, IdentityUser, yours may be called
// ApplicationUser if you have added any extra fields to the model
var userManager = context.RequestServices
.GetRequiredService<UserManager<IdentityUser>>();
var signInManager = context.RequestServices
.GetRequiredService<SignInManager<IdentityUser>>();
IdentityUser user = await userManager.GetUserAsync(context.User);
if(signInManager.IsSignedIn(context.User))
{
await signInManager.RefreshSignInAsync(user);
}
}
}
Then use it in your controller
[HttpPost("[action]")]
[Authorize]
public async Task<IActionResult> Validate()
{
await HttpContext.RefreshLoginAsync();
}
Or abstract it in an action filter
public class RefreshLoginAttribute : ActionFilterAttribute
{
public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
await context.HttpContext.RefreshLoginAsync();
await next();
}
}
Then use it like this in your controller
[HttpPost("[action]")]
[Authorize]
[RefreshLogin] // or simpler [Authorize, RefreshLogin]
public async Task<IActionResult> Validate()
{
// your normal controller code
}
This is also possible if the user is already logged out (their access token has expired but their refresh token is still valid).
Important note: the following only works if you have "Do you want to remember your user's devices?" set to "No" in the cognito config. If anyone knows how to get it to work with it on, please let me know.
We use the following flow (js client app connecting to .NET Core API):
User signs in using username/password (CognitoSignInManager<CognitoUser>.PasswordSignInAsync)
The client receives the token, userID, and refreshToken and stores them in localStorage.
When the original token expires (1 hour), the client gets a 401 error from the API.
The client calls another API endpoint with the userID and refreshToken which then in turn calls the code below on our user service.
If the refresh result is successful, we return the new token (AuthenticationResult.IdToken).
The client the repeats the call that originally errored in a 401 with the new token.
Here is the code we added to the User Service:
public async Task<UserLoginResult> SignInRefreshAsync(string uid, string refreshToken)
{
try
{
var result = await _cognitoIdentityProvider.InitiateAuthAsync(
new InitiateAuthRequest
{
AuthFlow = AuthFlowType.REFRESH_TOKEN_AUTH,
ClientId = _pool.ClientID,
AuthParameters = new Dictionary<string, string>
{
{ "REFRESH_TOKEN", refreshToken },
{ "SECRET_HASH", HmacSHA256(uid + _pool.ClientID, _options.UserPoolClientSecret) }
}
});
if (!result.HttpStatusCode.Successful() || string.IsNullOrEmpty(result.AuthenticationResult?.IdToken))
return new UserLoginResult(UserLoginStatus.Failed);
return new UserLoginResult(UserLoginStatus.Success, uid, null, null, result.AuthenticationResult.IdToken, null);
}
catch
{
return new UserLoginResult(UserLoginStatus.Failed);
}
}
private static string HmacSHA256(string data, string key)
{
using (var sha = new System.Security.Cryptography.HMACSHA256(System.Text.Encoding.UTF8.GetBytes(key)))
{
var result = sha.ComputeHash(System.Text.Encoding.UTF8.GetBytes(data));
return Convert.ToBase64String(result);
}
}
IAmazonCognitoIdentityProvider _cognitoIdentityProvider is resolved from DI.
AWSCognitoClientOptions _options = configuration.GetAWSCognitoClientOptions(); and IConfiguration configuration is also resolved from DI.
UserLoginResult is our class to hold the token and refresh token. Obviously, adjust accordingly.
Please note that setting SECRET_HASH may not be required based on your config is Cognito.
A few weeks ago I decided to start building an API for my system which is fronted by an MVC portal. I built Web Api capability into my existing MVC site by adding:
class WebApiConfig
{
public static void Register(HttpConfiguration configuration)
{
configuration.Routes.MapHttpRoute("API Default", "api/{controller}/{id}",
new { id = RouteParameter.Optional });
}
}
in my app_start folder, and modifying my Global.asax by adding:
GlobalConfiguration.Configure(WebApiConfig.Register);
It worked absolutely fine for calling simple methods in my Values controller either without the [Authorize] tag or in my browser by logging in first, but since then I've been reading around implementing basic authentication in asp.net web api and I've found a few examples I've tried to work into my implementation.
I have implemented a code example of a Message Handler I found online to authorize requests to it, at this stage simply comparing an ApiKey header string to one stored locally in my handler class to test it worked.
The handler looks like this:
protected override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request, CancellationToken cancellationToken)
{
IEnumerable<string> apiKeyHeaderValues = null;
if (request.Headers.TryGetValues("ApiKey", out apiKeyHeaderValues))
{
var apiKeyHeaderValue = apiKeyHeaderValues.First();
// ... your authentication logic here ...
var username = (apiKeyHeaderValue == "12345" ? "Authorised" : "OtherUser");
var usernameClaim = new Claim(ClaimTypes.Name, username);
var identity = new ClaimsIdentity(new[] { usernameClaim }, "ApiKey");
var principal = new ClaimsPrincipal(identity);
Thread.CurrentPrincipal = principal;
}
return base.SendAsync(request, cancellationToken);
}
I then added it to my global.asax:
GlobalConfiguration.Configuration.MessageHandlers.Add(new ApiAuthHandler());
Now I took this code in it's entirety from here: https://dzone.com/articles/api-key-user-aspnet-web-api as I'm new to this and lots of implementations of authorisation seemed too complex for my needs/too complex for me to begin my learning with.
I do understand it's flow and from debugging it when receiving a request it does the ApiKey comparison correctly and creates the principle. The response however is not correct...and it never reaches the requested api method. I get this response:
Redirect
To:http://localhost:2242/Account/Login?ReturnUrl=%2Fapi%2Fvalues with status: 302 Show explanation HTTP/1.1 302 Found
It is redirecting me to my register method, as the [Authorize] tag is meant to, and it's actually returning my Register.cshtml in it's entirety. I can't figure out how to ignore this and let the ApiAuthHandler Authorize for me. I'm assuming I need to change something in the MVC pipeline somewhere but I'm not sure what.
Just want to get something very simple working so that I can get my head around it more to explore more complicated API authentication. Any ideas what I've done wrong?
Edit added api controller:
public class ValuesController : ApiController
{
ApplicationDbContext context = new ApplicationDbContext();
InboundDBContext inboundContext = new InboundDBContext();
// GET api/<controller>
[Authorize]
public string Get()
{
return user.Identity.Name;
}
// GET api/<controller>/5
public string Get(int id)
{
return "value";
}
Your WebApiConfig.cs file should look something like this...
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
//add the authorization handler to the pipeline
config.MessageHandlers.Add(new new ApiAuthHandler()());
}
}
I see there are a lot of questions about this same topic, but since they are all from 2008 - 2011, I'd say there's a chance this might be an official way to do this without external libraries/extensions.
So the issue is when running my test cases, the ViewName comes empty:
// Act
ViewResult result = await Controller.Create(model) as ViewResult;
// Assert
Assert.AreEqual("Create", result.ViewName);
Any official way to deal with this? or maybe I can test some other property?
If your Controller Method does just
return View();
without a view name parameter value given, you will not have the name of the view in the ViewName property. For Unit Testing Controllers read: https://msdn.microsoft.com/en-us/library/ff847525(v=vs.100).aspx
How to create a controller with ControllerContext:
HomeController controller = new HomeController(repository);
controller.ControllerContext = new ControllerContext()
{
Controller = controller,
RequestContext = new RequestContext(new MockHttpContext(), new RouteData())
};
With:
private class MockHttpContext : HttpContextBase {
private readonly IPrincipal _user = new GenericPrincipal(
new GenericIdentity("someUser"), null /* roles */);
public override IPrincipal User {
get {
return _user;
}
set {
base.User = value;
}
}
}