First login to web site after Azure AD authentication - asp.net-core

I have an Asp.Net core 3.1 MVC web application. I use EF and Identity
I'm in the process of migrating the authentication process to Azure AD (organizational directory)
I need to find a way to know when the user is login to my site for the first time.
I want to add user-information such as prefered-color, name etc. and to store it in my local DB
My goal is to know when the user logs-in for the first time and redirect him to the profile page for the extra details
Basically, I do it with OnTicketReceived event.
In this event I get the current user after the login process completed, I get the user details from the token and check if the user already in my local DB.
Now I need to redirect it to his profile page if the user record was not found.
I cannot change the redirect URL in OnTicketReceived event, I decided to create a middleware as follows:
In OnTicketReceived I check if the user in my local DB and add "NewUser" key to the session
Added NewUserMiddleware that runs and checks the session key. If "NewUser" key exits, I generate a new record in my local DB and redirect to the profile page, then I remove the key
It's working but I'm sure that there is an easier way to do it...
I don't want to create a register button - the site is internal to the organization and I don't need to create an anonymous register page
Some code stuff:
in OnTicketReceived:
var db = context.HttpContext.RequestServices.GetService<MyDbContext>();
var user = db.Users.SingleOrDefault(u => u.Email == email);
if (user == null)
{
context.HttpContext.Session.SetInt32("NewUser", 1);
}
NewUserMiddleware:
public async Task InvokeAsync(HttpContext context)
{
int? isNewUser = context.Session.GetInt32("NewUser");
if(isNewUser.HasValue && isNewUser.Value == 1)
{
MyDbContext _dbContext = context.RequestServices.GetService(typeof(MyDbContext)) as MyDbContext;
var identity = context.User.Identity as ClaimsIdentity;
string email = identity.FindFirst(c => c.Type == "preferred_username")?.Value;
string name = identity.FindFirst(c => c.Type == "name")?.Value;
User user = new User()
{
Email = email,
UserName = name,
Color = GenerateColor()
};
_dbContext.Users.Add(user);
_dbContext.SaveChanges();
context.Session.Remove("NewUser");
await context.Session.CommitAsync();
context.Response.Redirect("account/profile", true);
}
await _next(context);
}

Related

How to extend and validate session in ASP.NET Core Identity?

We want to offer the users to manage their login sessions.
This worked so far pretty easy with ASP.NET Core and WITHOUT the Identity Extensions.
https://learn.microsoft.com/en-us/aspnet/core/security/authentication/cookie?view=aspnetcore-3.1#react-to-back-end-changes
But how can we invoke this validation with ASP.NET Core Identity?
Problem we have:
How do we store login-session-based information like Browser Version, Device Type and User Position? Do we extend any type or what is the idea?
How do we dynamically set the cookie expiration based on a specific user?
How do we invalidate the Cookie from the backend (like the link above shows)?
How do we required additional password-prompts for special functions?
It feels the ASP.NET Core Identity is still not that extensible and flexible :(
Unfortunately, this area of ASP.NET Identity is not very well documented, which I personally see as a risk for such a sensitive area.
After I've been more involved with the source code, the solution seems to be to use the SignIn process of the SignIn Manager.
The basic problem is that it's not that easy to get your custom claims into the ClaimsIdentity of the cookie. There is no method for that.
The values for this must under no circumstances be stored in the claims of the user in the database, as otherwise every login receives these claims - would be bad.
So I created my own method, which first searches for the user in the database and then uses the existing methods of the SignInManager.
After having a ClaimsIdentity created by the SignIn Manager, you can enrich the Identity with your own claims.
For this I save the login session with a Guid in the database and carry the id as a claim in the cookie.
public async Task<SignInResult> SignInUserAsync(string userName, string password, bool isPersistent, bool lockoutOnFailure)
{
DateTimeOffset createdLoginOn = DateTimeOffset.UtcNow;
DateTimeOffset validTo = createdLoginOn.AddSeconds(_userAuthOptions.ExpireTimeSeconds);
// search for user
var user = await _userManager.FindByNameAsync(userName);
if (user is null) { return SignInResult.Failed; }
// CheckPasswordSignInAsync checks if user is allowed to sign in and if user is locked
// also it checks and counts the failed login attempts
var attempt = await CheckPasswordSignInAsync(user, password, lockoutOnFailure);
if (attempt.Succeeded)
{
// TODO: Check 2FA here
// create a unique login entry in the backend
string browserAgent = _httpContextAccessor.HttpContext.Request.Headers["User-Agent"];
Guid loginId = await _eventDispatcher.Send(new AddUserLoginCommand(user.Id, user.UserName, createdLoginOn, validTo, browserAgent));
// Write the login id in the login claim, so we identify the login context
Claim[] customClaims = { new Claim(CustomUserClaims.UserLoginSessionId, loginId.ToString()) };
// Signin User
await SignInWithClaimsAsync(user, isPersistent, customClaims);
return SignInResult.Success;
}
return attempt;
}
With each request I can validate the ClaimsIdentity and search for the login id.
public class CookieSessionValidationHandler : CookieAuthenticationEvents
{
public override async Task ValidatePrincipal(CookieValidatePrincipalContext context)
{
ClaimsPrincipal userPrincipal = context.Principal;
if (!userPrincipal.TryGetUserSessionInfo(out int userId, out Guid sessionId))
{
// session format seems to be invalid
context.RejectPrincipal();
}
else
{
IEventDispatcher eventDispatcher = context.HttpContext.RequestServices.GetRequiredService<IEventDispatcher>();
bool succeeded = await eventDispatcher.Send(new UserLoginUpdateLoginSessionCommand(userId, sessionId));
if (!succeeded)
{
// session expired or was killed
context.RejectPrincipal();
}
}
}
}
See also
https://learn.microsoft.com/en-us/aspnet/core/security/authentication/cookie?view=aspnetcore-3.1#react-to-back-end-changes

OWIN Authentication Cookie information and forcing login even when not expired

I want to provide two pieces of functionality to an MVC 5 (OWIN) website for the Authentication Cookies:-
Show cookie expiration for each user
Force a login, even if a cookie is not expired
Once a user is logged in, the cookie and its contained claims are on their browser.
I've managed to get the cookie information, although it does this on every visit to the server.
As far as forcing a login is concerned, I either have to update the cookie (not possible if the user isn't interacting with the site) or look for a user by name on each login.
My answer is below - if anyone has a better one, please let me know
In the OWIN start-up class I added a custom identity validation method. An administrator can set a field in the Identity Database to force a login next time around. A downside is a hit on the database each time a user visits a web page.
If a user must login this time, Forced Login flag is cleared, the user logged off and redirected to the Home page which then goes to the login page (and redirect back to the Home page after login).
This code also retrieves the cookie information for display in the list of users that an administrator can view.
private static Task MyCustomValidateIdentity(CookieValidateIdentityContext context)
{
var db = ApplicationDbContext.Create();
var userName = context.Identity.GetUserName();
var user = (from u in db.Users where u.UserName == userName select u).FirstOrDefault();
if (user != null)
{
if (user.ForceLoginNextTime)
{
user.ForceLoginNextTime = false;
db.SaveChanges();
context.OwinContext.Authentication.SignOut(context.Options.AuthenticationType);
context.Response.Redirect("/Home");
}
}
var i = (from u in __userInfo where u.userName == userName select u).FirstOrDefault();
if (i == null)
{
i = new UserInfo();
__userInfo.Add(i);
}
i.userName = userName;
i.CookieIssuedUtc = context.Properties.IssuedUtc;
i.CookieExpiresUtc = context.Properties.ExpiresUtc;
i.CookieIsPersistent = context.Properties.IsPersistent;
return Task.FromResult(0);
}

Unable to login with users created via dbContext in asp.net core mvc

I am trying to seed the db with initial data and I am using the following code to create the users. Users get created, passwords hashed, etc but when I try to login with my password, it fails to log me in with error message: Invalid login attempt. What am I doing wrong? I am using asp.net core mvc application with identity template, not a custom login.
var mymail = "my#my.com";
var mypw = "Test1.";
var applicationUsers = new ApplicationUser[]
{
new ApplicationUser {
UserName = Constants.AnonUserName,
Email = "Anonymous#xyz.com"
},
new ApplicationUser {
UserName = mymail,
Email = mymail
}
};
var pwHasher = new PasswordHasher<ApplicationUser>();
applicationUsers.ToList().ForEach(u =>
{
u.PasswordHash = pwHasher.HashPassword(u, mypw);
context.ApplicationUsers.Add(u);
});
context.SaveChanges();
Login fails because NormalizedUserName field in db is null and during login, following query is issued (which fails):
SELECT "u"."Id", "u"."AccessFailedCount", "u"."ConcurrencyStamp", "u"."Email", "u"."EmailConfirmed", "u"."LockoutEnabled", "u"."LockoutEnd", "u"."NormalizedEmail", "u"."NormalizedUserName", "u"."PasswordHash", "u"."PhoneNumber", "u"."PhoneNumberConfirmed", "u"."SecurityStamp", "u"."TwoFactorEnabled", "u"."UserName"
FROM "AspNetUsers" AS "u"
WHERE "u"."NormalizedUserName" = $1
LIMIT 1
DETAIL: parameters: $1 = 'MY#MY.COM'
I guess the solution is to inject ILookupNormalizer service and normalize with its Normalize method but it is already too much work. I am injecting UserManager service and using its CreateAsync method to create a user with a password as advised by #Tseng above in comments.

How to add and Persist a new ClaimsIdentity with custom claims to a ClaimsPrincipal after authentication by an OpenId Connect provider

I'm using OpenId Connect with .NetCore to send users to an external OpenId Connect authentication provider (OP) which returns relevant access tokens and an authenticated ClaimsPrinciple which has claims from the OP in such as Name, Email Address, Customer Id, etc. What I want to be able to do is once the User has been authenticated and the ClaimsPrinciple is returned from the OP, I would like to add a ClaimsIdentities with custom claims for each licence the user holds on there account to the ClaimsPrinciple. So when a user switches between their account licences I can access the correct identity for that licence and provide access to features based on the custom claims. Currently, I can add the custom claims to a ClaimsIdentity then add the ClaimsIdentity to the ClaimsPrinciple but the new identities are not persisted and added into the Cookie.
So A User can have multiple licences.
For each licence I want to add a ClaimsIdentity.
Then I want to persist the changes to the ClaimsPrinciple by using a cookie.
Here is a code snippet which will hopefully add some context. This is for the Login method in my application which is hit once the OP has authenticated the user.
public async Task<IActionResult> Login()
{
string token = HttpContext.Authentication.GetTokenAsync("access_token").Result;
string refreshToken = HttpContext.Authentication.GetTokenAsync("refresh_token").Result;
// ControllerBase User class this is the User I want to add the identites to, I think.
string userGuid = User.Claims.Where(c => c.Type == "Guid").FirstOrDefault().Value;
UserTokens userTokens = new UserTokens
{
LastUpdatedDate = DateTime.Now,
UserGuid = userGuid,
UserAccessToken = token,
RefreshToken = refreshToken
};
await _busClient.PublishAsync<UpdateUserTokens>(new UpdateUserTokens(userTokens));
// Domain Model User, a different User to the controllerBase User
// this is how a user is represented in my application but this has
// no control over authentication and claims
User user = await _requestClient.RequestAsync<UserGuidRequest, User>(new UserGuidRequest(userGuid));
var userLicences = await _requestClient.RequestAsync<UserLicenceRequest, List<UserLicence>>(new UserLicenceRequest(userGuid));
var identityServerUserClaims = User.Claims.ToList();
foreach(UserLicence userLicence in userLicences)
{
List<Claim> userLicenceIdentityClaims = new List<Claim>();
foreach (Claim claim in identityServerUserClaims)
{
userLicenceIdentityClaims.Add(claim);
}
userLicenceIdentityClaims.Add(new Claim(userLicence.RoleType.ToString(), ""));
var userLicenceIdentity = new ClaimsIdentity(userLicenceIdentityClaims, User.Identity.AuthenticationType);
userLicenceIdentity.Label = userLicence.Id.ToString();
User.AddIdentity(userLicenceIdentity);
}
//TODO: either need to sign out then back in or save new user claims to cookies somehow?
return View("Index", user);
}
If anyone could help it would be greatly appreciated. If you need any more information please ask and I'll try to provide whatever I can, hopefully, this is a good start.

WebSecurity.CurrentUserName and User.Identity.Name are null after logging in

I'm stating to figure out SimpleMembership for my ASP.Net MVC 4 site. I've augmented UserProfile with a custom property, AccountTypeId. I've updated the database table with the augmented property and can save data to the database when registering. I'm a bit confused about how to retrieve data about the user once they have logged in.
In my Account controller, I have a Login action that gets posted to when a user logs in. Here's my code:
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public ActionResult Login(LoginModel model, string returnUrl)
{
if (ModelState.IsValid && WebSecurity.Login(model.UserName, model.Password, persistCookie: model.RememberMe))
{
var userName = WebSecurity.CurrentUserName;
var identityName = User.Identity.Name;
var currentuserid = WebSecurity.GetUserId(model.UserName);
var context = new UsersContext();
var user = context.UserProfiles.SingleOrDefault(u => u.UserId == currentuserid);
var accountTypeId = user.AccountTypeId;
return RedirectToLocal(returnUrl);
}
// If we got this far, something failed, redisplay form
ModelState.AddModelError("", "The user name or password provided is incorrect.");
return View(model);
}
WebSecurity.CurrentUserName and User.Identity.Name are both empty strings, however, I can retrieve the UserId using WebSecurity.GetUserId(model.UserName) and can therefore retrieve the user data and I can get accountTypeId.
What's strange is User.Identity.Name gets displayed on my page when it's being called from a .cshtml page after the user is redirected to the landing page. So, somewhere in between the Login action of my controller and the destination page, User.Identity is getting set with data.
I'm assuming since I'm past the WebSecurity.Login check, that WebSecurity would have information about the logged in user, but it doesn't seem to be that way.
What am I missing?
The username is written to a cookie. In order for cookie data to be available, it must be placed into the Response and sent back to the browser. Upon the next request, the cookie value will be read and used to populate the User.Identity.Name property.
In other words, the User.Identity.Name property should be an empty string until after your Redirect call. This is the purpose of redirecting after signing on: to write the cookie to the browser so that subsequent requests will treat the user as signed on.