I'm currently trying to learn unit tests and I have created project in ASP.NET Core, so I can learn testing on real example. I want to test happy path for authenticate method in API Controller, so it will return OkObjectResult.
What I have so far.
Controller method i'd like to test
[AllowAnonymous]
[HttpPost("authenticate")]
public IActionResult Authenticate([FromBody]User userParam)
{
var user = _userService.Authenticate(userParam.Nickname,
userParam.Password).Result;
if(user == null)
{
return BadRequest(
new { message = "Username or password is incorrect " }
);
}
return Ok(user);
}
Authenticate method in class that implements IUserService:
public async Task<User> Authenticate(string nickname, string password)
{
var user = _repository.User.GetAllUsersAsync().Result.SingleOrDefault(u => u.Nickname == nickname && u.Password == password);
if(user == null)
{
return null;
}
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes(_appSettings.Secret);
var tokenDescription = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new Claim[]
{
new Claim(ClaimTypes.Name, user.UserId.ToString())
}),
Expires = DateTime.UtcNow.AddDays(7),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
};
var token = tokenHandler.CreateToken(tokenDescription);
user.Token = tokenHandler.WriteToken(token);
try
{
await _repository.User.UpdateUserAsync(user);
user.Password = null;
return user;
}
catch (Exception e)
{
return user;
}
}
And my unit test:
[Fact]
public void Authenticate_WhenCalled_ReturnsOk()
{
//Arrange
var mockService = new Mock<IUserService>();
var user = new User()
{
UserId = 4,
IsAdmin = true,
Token = "12983912803981",
IsLogged = true,
MessagesSent = null,
MessagesReceived = null,
Nickname = "test3",
Password = "Str0ngP#ssword123",
UserChannels = null
};
var controller = new UsersController(_repository, _logger, mockService.Object);
//Act
var result = controller.Authenticate(user);
//Assert
var okResult = result.Should().BeOfType<OkObjectResult>();
}
However, unit tests is returning BadRequest, not OkObjectResult as intended.
That means probably that user is actually null and it's throwing a BadRequest. Should I mock Repository instead of IUserService?
Actually, you are pretty good and doing everything perfectly (too few developers actually using AAA, which is very sad) but do remember that Mock by default returns default(T) value. So your Authenticate method is mocked and return default(User) which is null.
Just make it return your stub user:
var mockService = new Mock<IUserService>();
var user = new User()
{
UserId = 4,
IsAdmin = true,
Token = "12983912803981",
IsLogged = true,
MessagesSent = null,
MessagesReceived = null,
Nickname = "test3",
Password = "Str0ngP#ssword123",
UserChannels = null
};
mockService.Setup(x=> x.Authenticate(It.IsAny(), It.IsAny())).Returns(user);
Or more strict version proposed by #xander:
mockService.Setup(x=> x.Authenticate("test3", "Str0ngP#ssword123")).Returns(user);
It will also check that you actually using values from passed in User and not just blindly return Ok().
Related
In my controller I hard-coded a username and password. Now I want to allow the user to login with his/her Azure AD username and password.
public IActionResult Login([FromBody] LoginModel user)
{
if (user == null)
{
return BadRequest("Invalid client request");
}
if (user.UserName == "johndoe" && user.Password == "def#MKK")
{
var secretKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("justAsecretKey"));
var signinCredentials = new SigningCredentials(secretKey, SecurityAlgorithms.HmacSha256);
var claims = new List<Claim>
{
new Claim(ClaimTypes.Name, user.UserName)
};
var token = new JwtSecurityToken(
audience: "http://api.azurewebsites.net",
issuer: "http://api.azurewebsites.net",
claims: claims,
expires: DateTime.Now.AddMinutes(60),
signingCredentials: signinCredentials
);
var results = new
{
token = new JwtSecurityTokenHandler().WriteToken(token),
expiration = token.ValidTo
};
return Ok(results);
}
else
{
return Unauthorized();
}
}
My Solution:
I got rid of the hardcoded User
Added Credentials validation
Provide the Azure AD Configurations
Set the scopes/api permissions
AcquireTokenByUsernamePassword
Return the Tokens and other info when succesful
Otherwise return BadRequest or Unauthorized
public ActionResult Login([FromBody] UserModel user)
{
if (user == null)
{
var results = new
{
Message = "Invalid client request"
};
return BadRequest(results);
}
if (user.UserName != "" && user.Password != "")
{
try
{
string clientId = Configuration["AzureAd:ClientId"];
string tenantId = Configuration["AzureAd:TenantId"];
string authority = Configuration["AzureAd:Authority"];
string[] scopes = new string[] { "User.Read", "Directory.AccessAsUser.All", "User.Invite.All" };
IPublicClientApplication app = PublicClientApplicationBuilder.Create(clientId)
.WithAuthority(authority)
.WithTenantId(tenantId)
.Build();
var securePassword = new SecureString();
foreach (char c in user.Password.ToCharArray()) // you should fetch the password
securePassword.AppendChar(c); // keystroke by keystroke
var tokens = app.AcquireTokenByUsernamePassword(scopes, user.UserName, securePassword).ExecuteAsync().Result;
graphClient = new GraphServiceClient(
new DelegateAuthenticationProvider(x =>
{
x.Headers.Authorization = new AuthenticationHeaderValue(
"Bearer", tokens.AccessToken);
return Task.FromResult(0);
}));
var results = new
{
ExpiresOn = tokens.ExpiresOn.LocalDateTime,
AccessToken = tokens.AccessToken,
Token = tokens.IdToken,
Environment = tokens.Account.Environment,
AccountId = tokens.Account.HomeAccountId
};
return Ok(results);
}
catch (Exception ex)
{
var results = new
{
Message = ex.Message
};
return Unauthorized(results);
}
}
else
{
var results = new
{
Message = "Username or password isn't valid."
};
return Unauthorized(results);
}
}
trying to authenticate a token through a post request, user always returns null when i check via the username.
public async Task<IActionResult> Login([FromBody] LoginModel model)
{
var user = await _userManager.FindByNameAsync(model.UserName); // user always equals null here
if (user !=null && await _userManager.CheckPasswordAsync(user,model.UserName))
{
var claims = new[]
{
new Claim(JwtRegisteredClaimNames.Sub, user.UserName),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
};
var signingKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("MySuperSecureKey"));
var token = new JwtSecurityToken(
issuer: "http://blah.com",
audience: "http://blah.com",
expires: DateTime.UtcNow.AddHours(1),
claims: claims,
signingCredentials: new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256)
);
return Ok(new
{
token = new JwtSecurityTokenHandler().WriteToken(token),
expiration = token.ValidTo
});
}
return Unauthorized();
}
wondering if the problem is coming from how i seeded my database with this dummy data class?
public class DummyData
{
public static void Initialize(IApplicationBuilder app, IServiceProvider serviceProvider)
{
using (var serviceScope = app.ApplicationServices.GetService<IServiceScopeFactory>().CreateScope())
{
var context = serviceScope.ServiceProvider.GetService<HealthContext>();
var userManager = serviceProvider.GetRequiredService<UserManager<ApplicationUser>>();
context.Database.EnsureCreated();
//context.Database.Migrate();
// Look for any ailments
if (context.Ailments != null && context.Ailments.Any())
return; // DB has already been seeded
var ailments = GetAilments().ToArray();
context.Ailments.AddRange(ailments);
context.SaveChanges();
var medications = GetMedications().ToArray();
context.Medications.AddRange(medications);
context.SaveChanges();
var patients = GetPatients(context).ToArray();
context.Patients.AddRange(patients);
context.SaveChanges();
if (!context.Users.Any())
{
ApplicationUser user = new ApplicationUser()
{
Email = "test#test.com",
SecurityStamp = Guid.NewGuid().ToString(),
UserName = "testTest"
};
userManager.CreateAsync(user, "Password123");
context.Users.Add(user);
context.SaveChanges();
}
}
}
After I seed my database i can see my the user created in the fields, its defo there, so not understanding what seems to be the issue here!
The first thing is to check the database to confirm whether your user is successfully created . There are several issues you can check firstly :
For userManager.FindByNameAsync confirm that you are typing the AndyCosStav , not the email .
The CheckPasswordAsync returns a flag indicating whether the given password is valid for the specified user.
public virtual System.Threading.Tasks.Task<bool> CheckPasswordAsync (TUser user, string password);
The second parameter should be the password , i notice you are checking with username :
if (user !=null && await _userManager.CheckPasswordAsync(user,model.UserName))
Password123 may not match the defalut ASP.NET Core identity password
restrict , you must have at least one non alphanumeric character.
what i am testing.
This is an identity server project with a login to federated gateway. I do not control this gateway and am having issues with them not returning the proper claims back to me that i need to verify the users logins. I would like to be able to test that i can handle these errors.
For example email claim is missing without that i can not login a user.
I have created a test that tests the email claim is missing returns an error.(Works fine)
Now I am trying to test the other side of things. If the claims are in fact there it should return the user that matches to the claims returned.
The method we are testing
public static async Task<(ApplicationUser user, string provider, string providerUserUserName, IEnumerable<Claim> claims, string message)> FindUserFromExternalProvider(AuthenticateResult result, UserManager<ApplicationUser> userManager, ILogger<SegesExternalController> logger)
{
var externalUser = result.Principal;
// try to determine the unique id of the external user (issued by the provider)
var eMailClaim = externalUser.FindFirst(SegesSettingsConstants.SegesEmailClaimName);
if(eMailClaim == null) return (null, null, null, null, $"{SegesSettingsConstants.SegesEmailClaimName} claim not found.");
// remove the user id claim so we don't include it as an extra claim if/when we provision the user
var claims = externalUser.Claims.ToList();
claims.LogSegesClaims(logger);
claims.Remove(eMailClaim);
// Should we remove more claims
var provider = result.Properties.Items["scheme"];
var providerUserUserName = eMailClaim.Value;
var user = await userManager.FindByEmailAsync(providerUserUserName); // Test Breaks here
return (user, provider, providerUserUserName, claims, null);
}
Test
[Fact]
public async void Federated_login_with_email_claim_return_no_error()
{
// Arrange
var principal = new ClaimsPrincipal();
principal.AddIdentity(new ClaimsIdentity(
new Claim[] {
new Claim("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name", "Testbruger til André"),
new Claim("http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname", #"PROD\Salg43"),
new Claim("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/postalcode", "8200"),
new Claim("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/locality", "Aarhus N"),
new Claim("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress", "test#email.com"),
},
"FakeScheme"));
var authenticateResult = AuthenticateResult.Success(new AuthenticationTicket(principal, new AuthenticationProperties() { Items = { { "scheme", "fed" } } }, "FakeScheme"));
var exprectUser = new ApplicationUser()
{
UserName = "test#email.com",
NormalizedUserName = "TEST#EMAIL.COM",
NormalizedEmail = "TEST#EMAIL.COM",
Email = "test#email.com",
Id = 123,
EmailConfirmed = true
};
var mockEmailStore = new Mock<IUserEmailStore<ApplicationUser>>();
var mockQueryableUserStore = new Mock<IQueryableUserStore<ApplicationUser>>();
var mockUserStore = new Mock<IUserStore<ApplicationUser>>();
mockUserStore.Setup(x => x.FindByIdAsync(exprectUser.Id.ToString(), CancellationToken.None)).ReturnsAsync(exprectUser);
var userManager = new UserManager<ApplicationUser>(mockUserStore.Object, null, null, null, null, null, null, null, null);
var logger = new Logger<ExternalController>(new LoggerFactory());
// Act
var (user, provider, providerUserUserName, claims, errorMessage) = await AuthorizationHelpers.FindUserFromExternalProvider(authenticateResult, userManager, logger);
// Assert
user.ShouldNotBeNull();
}
The issue with above.
I am trying to moq a usermanager for my unit test
var exprectUser = new ApplicationUser()
{
UserName = "test#email.com",
NormalizedUserName = "TEST#EMAIL.COM",
NormalizedEmail = "TEST#EMAIL.COM",
Email = "test#email.com",
Id = 123,
EmailConfirmed = true
};
var mockUserStore = new Mock<IUserStore<ApplicationUser>>();
mockUserStore.Setup(x => x.FindByIdAsync(exprectUser.Id.ToString(), CancellationToken.None)).ReturnsAsync(exprectUser);
var userManager = new UserManager<ApplicationUser>(mockUserStore.Object, null, null, null, null, null, null, null, null);
however when the method i am testing tries to find the user.
var findUser = await userManager.FindByEmailAsync("test#test.com");
it throws an error
Message: System.NotSupportedException : Store does not implement IUserEmailStore.
How do i implement IUserEmailStore in my moq usermanager?
My unit test project does contain the newest EntityFramework package.
Trying another way.
var founduser = userManager.Users.FirstOrDefault(e => e.Email.Equals("test#test.com", StringComparison.InvariantCultureIgnoreCase));
results in
System.NotSupportedException : Store does not implement IQueryableUserStore.
I think i must be moqing this wrong.
Update From comment
Ok i can moq the IUserEmailStore but I am not sure what i should do with it
var mockEmailStore = new Mock<IUserEmailStore<ApplicationUser>>();
I managed to create a full moq usermanager that lets me search on email
public class MoqUserManager : UserManager<ApplicationUser>
{
public MoqUserManager(IUserStore<ApplicationUser> userStore) : base(userStore,
new Mock<IOptions<IdentityOptions>>().Object,
new Mock<IPasswordHasher<ApplicationUser>>().Object,
new IUserValidator<ApplicationUser>[0],
new IPasswordValidator<ApplicationUser>[0],
new Mock<ILookupNormalizer>().Object,
new Mock<IdentityErrorDescriber>().Object,
new Mock<IServiceProvider>().Object,
new Mock<ILogger<UserManager<ApplicationUser>>>().Object)
{ }
public override Task<ApplicationUser> FindByEmailAsync(string email)
{
return Task.FromResult(new ApplicationUser { Email = email });
}
}
which gives me
var mockUserStore = new Mock<IUserStore<ApplicationUser>>();
mockUserStore.Setup(x => x.FindByIdAsync(exprectUser.Id.ToString(), CancellationToken.None)).ReturnsAsync(exprectUser);
var userManager = new FakeUserManager(mockUserStore.Object);
So now i can verify that the proper user is returned from my identity server matching the federated login user.
Okay your with the updated question the issue lies in
var userManager = new UserManager<ApplicationUser>(mockUserStore.Object, null, null, null, null, null, null, null, null);
This is not creating a mock, but an actual instance of UserManager<T>.
You will have to do
var userManagerMock = new Mock<UserManager<ApplicationUser>>(mockUserStore.Object, null, null, null, null, null, null, null, null);
then do an setup
userManagerMock.Setup(um => um.FindByEmailAsync("test#email.com)).Returns(exprectUser)
and pass userManagerMock.Object to your
var (user, provider, providerUserUserName, claims, errorMessage) = await AuthorizationHelpers.FindUserFromExternalProvider(authenticateResult, userManagerMock.Object, logger);
When mocking, you never want to call new on the external dependency and instead mock it, since then you can't change its behavior for a specific test. UserManager<T> should have all or most public properties as virtual, so you can override them.
I am authenticating a user:
[Route("Login"), HttpPost, AllowAnonymous]
public LoginViewModelResponse Login(LoginViewModelRequest data)
{
if(!Membership.ValidateUser(data.Username, data.Password))
{
return new LoginViewModelResponse
{
DisplayMessage = "Invalid Username/Password!",
IsSuccess = false,
RedirectUrl = "/Home/"
};
}
FormsAuthentication.SetAuthCookie(data.Username, false);
ClaimsIdentity identity = new GenericIdentity(data.Username);
var roles = "Administrator,User".Split(',');
// var client = AuthorisationService.instance.GetAuthenticatedUser();// new ClientService().GetClientById(1);
var principle = new GenericPrincipal(identity, roles);
HttpContext.Current.User = principle;
System.Threading.Thread.CurrentPrincipal = principle;
if (User.IsInRole("Administrator"))
{
var b = 1;
}
return new LoginViewModelResponse
{
IsSuccess = true,
DisplayMessage = "OK",
RedirectUrl = "/Home/"
};
}
And the test for 'IsInRole' is working.
However, I have the following in my View (_layout), and the check for Administrator fails.
if (ViewContext.HttpContext.User.IsInRole("Administrator"))
{
<li class="dropdown">
...
Is there something I need to do to allow the View to understand "IsInRole"?
This works:
#if (ViewContext.HttpContext.User.Identity.IsAuthenticated == false)
But 'IsInRole' always evaluated to false.
Since you set FormsAuthentication cookie by yourself, you'll need to create Principle object and assign it to current thread on every request inside AuthenticateRequest event.
Global.asax.cs
public class Global : HttpApplication
{
protected void Application_AuthenticateRequest(object sender, EventArgs e)
{
HttpCookie decryptedCookie =
Context.Request.Cookies[FormsAuthentication.FormsCookieName];
if (decryptedCookie != null)
{
FormsAuthenticationTicket ticket =
FormsAuthentication.Decrypt(decryptedCookie.Value);
var identity = new GenericIdentity(ticket.Name);
var roles = ticket.UserData.Split(',');
var principal = new GenericPrincipal(identity, roles);
HttpContext.Current.User = principal;
Thread.CurrentPrincipal = HttpContext.Current.User;
}
}
}
Sign-In method
public void SignIn(string username, bool createPersistentCookie)
{
var now = DateTime.UtcNow.ToLocalTime();
TimeSpan expirationTimeSpan = FormsAuthentication.Timeout;
var ticket = new FormsAuthenticationTicket(
1 /*version*/,
username,
now,
now.Add(expirationTimeSpan),
createPersistentCookie,
"" /*userData*/,
FormsAuthentication.FormsCookiePath);
var encryptedTicket = FormsAuthentication.Encrypt(ticket);
var cookie = new HttpCookie(FormsAuthentication.FormsCookieName,
encryptedTicket)
{
HttpOnly = true,
Secure = FormsAuthentication.RequireSSL,
Path = FormsAuthentication.FormsCookiePath
};
if (ticket.IsPersistent)
{
cookie.Expires = ticket.Expiration;
}
if (FormsAuthentication.CookieDomain != null)
{
cookie.Domain = FormsAuthentication.CookieDomain;
}
Response.Cookies.Add(cookie);
}
Code is as follows
startup auth:
app.UseFacebookAuthentication(new FacebookAuthenticationOptions
{
AppId = "xxx",
AppSecret = "xxx",
BackchannelHttpHandler = new FacebookBackChannelHandler(),
UserInformationEndpoint = "https://graph.facebook.com/v2.4/me?fields=id,name,email,first_name,last_name,location",
Scope = { "email" }
});
in the accountcontroler RegisterExternal class i call the following:
var info = await AuthenticationManager_GetExternalLoginInfoAsync_WithExternalBearer();
Which is this class:
private async Task<ExternalLoginInfo> AuthenticationManager_GetExternalLoginInfoAsync_WithExternalBearer()
{
ExternalLoginInfo loginInfo = null;
var result = await Authentication.AuthenticateAsync(DefaultAuthenticationTypes.ExternalBearer);
if (result != null && result.Identity != null)
{
var idClaim = result.Identity.FindFirst(ClaimTypes.NameIdentifier);
if (idClaim != null)
{
loginInfo = new ExternalLoginInfo()
{
DefaultUserName = result.Identity.Name == null ? "" : result.Identity.Name.Replace(" ", ""),
Login = new UserLoginInfo(idClaim.Issuer, idClaim.Value),
};
}
}
return loginInfo;
}
This is because the RegisterExternal class on default will use authentication type cookie. Whenever i use it it will return null, so after surfing the web I've noticed it is necessary add this code which in turn will use the bearer for authentication, this will result in a reply where the username and the identity are no longer null, thus authorized. (see picture below)
Return object (username and login)
But, when i want to claim the email, i cannot do this. It will always return null no matter what i do.
I was running into the same issue and solved it by using the Facebook nuget package to get extra fields.
In my application I have implemented FacebookAuthenticationProvider and overridden the Authenticated(...) method with the following:
public class FacebookAuthProvider : FacebookAuthenticationProvider
{
public override Task Authenticated(FacebookAuthenticatedContext context)
{
var accessTokenClaim = new Claim("ExternalAccessToken", context.AccessToken, "urn:facebook:access_token");
context.Identity.AddClaim(accessTokenClaim);
var extraClaims = GetAdditionalFacebookClaims(accessTokenClaim);
context.Identity.AddClaim(new Claim(ClaimTypes.Email, extraClaims.First(k => k.Key == "email").Value.ToString()));
context.Identity.AddClaim(new Claim("Provider", context.Identity.AuthenticationType));
context.Identity.AddClaim(new Claim(ClaimTypes.Name, context.Identity.FindFirstValue(ClaimTypes.Name)));
var userDetail = context.User;
var link = userDetail.Value<string>("link") ?? string.Empty;
context.Identity.AddClaim(new Claim("link", link));
context.Identity.AddClaim(new Claim("FacebookId", userDetail.Value<string>("id")));
return System.Threading.Tasks.Task.FromResult(0);
}
private static JsonObject GetAdditionalFacebookClaims(Claim accessToken)
{
var fb = new FacebookClient(accessToken.Value);
return fb.Get("me", new { fields = new[] { "email", "first_name", "last_name" } }) as JsonObject;
}
}
My Startup.cs has this within Configuration(IAppBuilder app):
FacebookAuthOptions = new Microsoft.Owin.Security.Facebook.FacebookAuthenticationOptions
{
AppId = "YOUR APP ID",
AppSecret = "YOUR APP SECRET",
Microsoft.Owin.PathString("/Account/ExternalCallBack"), // whatever your external callback url is
Provider = new FacebookAuthProvider()
};
FacebookAuthOptions.Scope.Add("email");
app.UseFacebookAuthentication(FacebookAuthOptions);
I've no idea why the email was not being populated in the first place, but this approach worked for me.