I have a controller method called UserSignIn that's used to authenticate a user. The "Candidate" parameter is a model that contains fields including the contact's email address and password.
The model also contains fields "AgencyID" and "ContactID". These are used so that I know which database to connect to (AgencyID) and which contact record to get (ContactID). The user signing in is a contact at an agency.
[HttpPost()]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> UserSignIn(Candidate can)
{
bool is_err = false;
string err = string.Empty;
Candidate c_signed_in = new Candidate();
// check data
if (string.IsNullOrEmpty(can.Email))
{
is_err = true;
err += "<li>Missing email address.</li>";
}
if (string.IsNullOrEmpty(can.AccountPassword))
{
is_err = true;
err += "<li>Missing password.</li>";
}
// get candidate
if (ModelState.IsValid && !is_err)
{
c_signed_in = await Repository.GetCandidate(can.AgencyID, 0, can.Email.ToLower(), can.AccountPassword, hostingEnv.WebRootPath);
if (c_signed_in.ContactID == 0)
{
is_err = true;
err += "<li>No account found. Check your credentials.</li>";
}
}
// check model state
if (!ModelState.IsValid || is_err)
{
Candidate c_current = await Repository.GetBlankCandidate(can, false);
c_current.IsModeSignIn = true;
if (is_err)
c_current.ErrsSignIn = "<ul class=\"text-danger\">" + err + "</ul>";
return View("Agency", c_current);
}
// create claims
var claims = new List<Claim>
{
//new Claim(ClaimTypes.Name, c_signed_in.FirstName + gFunc.SPACE + c_signed_in.FamilyName),
new Claim(ClaimTypes.Sid, c_signed_in.ContactID.ToString()),
new Claim(ClaimTypes.Email, c_signed_in.Email)
};
// create identity
var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme); // cookie or local
// create principal
ClaimsPrincipal principal = new ClaimsPrincipal(new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme));
// sign-in
await HttpContext.SignInAsync(scheme: CookieAuthenticationDefaults.AuthenticationScheme, principal: principal);
// add to log
gFunc.AddLogEntry("SignIn Candidate: " + c_signed_in.FirstName + gFunc.SPACE + c_signed_in.FamilyName + " - " + c_signed_in.Email);
// fini
return RedirectToAction("Profile", new { agencyID = c_signed_in.AgencyID, contactID = c_signed_in.ContactID });
}
On success, this method redirects to a method called "Profile" that displays the user's profile.
[HttpGet]
[Authorize]
public async Task<ActionResult> Profile(int agencyID, int contactID)
{
Candidate can = await Repository.GetCandidate(agencyID, contactID, string.Empty, string.Empty, hostingEnv.WebRootPath);
if (can.ContactID == 0)
{
int id = agencyID;
return RedirectToAction("Agency", new { agencyID = id });
}
return View("Profile", can);
}
My URL is now "/Home/Profile?agencyID=5809&contactID=19492
However, I can now just change the contactID in the URL and now I'm on another user's profile without being authorized.
How do I avoid this? Obviously I can't include the password as a parameter in the Profile method because it would simply be visible in the URL. What approach should I be taking?
UPDATE - SOLVED
Thanks to all for your comments. Camilo Terevinto's answer solved my problem.
I added the info I needed to the claims in the UserSignIn method and removed the parameters in the Profile method, where I can retrieve the info I need from the active user. Now I can ensure that only the authorized user can reach the "Profile" controller method.
The only thing I had to change was the direct int cast. My compiler didn't like it, so I just change it to use a parse instead:
int agency_id = int.Parse(User.FindFirst(ClaimTypes.NameIdentifier).Value);
int contact_id = int.Parse(User.FindFirst(ClaimTypes.Sid).Value);
You can add agencyID and contactID to Claims:
new Claim(ClaimTypes.Sid, c_signed_in.ContactID.ToString()),
new Claim(ClaimTypes.Email, c_signed_in.Email),
new Claim(ClaimTypes.NameIdentifier,c_signed_in.agencyID.ToString())
In controller you can obtain it from logged user data:
[HttpGet]
[Authorize]
public async Task<ActionResult> Profile()
{
int agencyID = (int)User.FindFirst(ClaimTypes.NameIdentifier).Value
int contactID = (int) User.FindFirst(ClaimTypes.Sid).Value
Candidate can = await Repository.GetCandidate(agencyID, contactID, string.Empty, string.Empty, hostingEnv.WebRootPath);
if (can.ContactID == 0)
{
int id = agencyID;
return RedirectToAction("Agency", new { agencyID = id });
}
return View("Profile", can);
}
Related
I am using itfoxtec-identity-saml2 in my Dotnet 3.1 Project. I am initiating request from server and validating the login till here everything is working fine.
After getting response assertion from server and getting claims transformed and creating a session but still my application is unable to login.
Below are snippets of my code for reference.
AuthController.cs
[Route("AssertionConsumerService")]
public async Task<IActionResult> AssertionConsumerService()
{
try
{
var binding = new Saml2PostBinding();
var saml2AuthnResponse = new Saml2AuthnResponse(config);
binding.ReadSamlResponse(Request.ToGenericHttpRequest(), saml2AuthnResponse);
if (saml2AuthnResponse.Status != Saml2StatusCodes.Success)
{
throw new AuthenticationException($"SAML Response status: {saml2AuthnResponse.Status}");
}
binding.Unbind(Request.ToGenericHttpRequest(), saml2AuthnResponse);
await saml2AuthnResponse.CreateSession(HttpContext, claimsTransform: (claimsPrincipal) => ClaimsTransform.TransformClaims(claimsPrincipal),isPersistent:true, lifetime: new TimeSpan(1, 0, 0));
var auth = HttpContext.User.Identity.IsAuthenticated;
}
catch (Exception ex)
{
}
return Redirect("~/");
}
ClaimsTransform.cs
public static ClaimsPrincipal TransformClaims(ClaimsPrincipal claimsPrincipal)
{
ClaimsIdentity identity = (ClaimsIdentity)claimsPrincipal.Identity;
var tenantId = identity.FindFirst(ClaimTypes.NameIdentifier);
var Name = identity.FindFirst("firstName");
var firstName = identity.FindFirst("firstName");
var Email = identity.FindFirst("Email");
var UserID = identity.FindFirst("UserID");
var claimsToKeep = new List<Claim> { tenantId, Name,firstName, Email, UserID };
var newIdentity = new ClaimsIdentity(claimsToKeep, identity.AuthenticationType, ClaimTypes.NameIdentifier, ClaimTypes.Role);
ClaimsPrincipal newClaims = new ClaimsPrincipal(newIdentity);
return new ClaimsPrincipal(new ClaimsIdentity(claimsToKeep, identity.AuthenticationType, ClaimTypes.Name, ClaimTypes.Role)
{
BootstrapContext = ((ClaimsIdentity)claimsPrincipal.Identity).BootstrapContext
});
//return newClaims;
}
After all this my application is redirecting back to login page instead of home page of the application with logged in user.
Help will be appreciated.
You need to set the users identity claim to a claim which exist in the claim set, otherwise the user is not accepted as being authenticated.
If eg. the tenantId claim is the users identity then the users identity claim is ClaimTypes.NameIdentifier in new ClaimsPrincipal(... ClaimTypes.NameIdentifier, ClaimTypes.Role)
ClaimsTransform.cs
public static ClaimsPrincipal TransformClaims(ClaimsPrincipal claimsPrincipal)
{
ClaimsIdentity identity = (ClaimsIdentity)claimsPrincipal.Identity;
var tenantId = identity.FindFirst(ClaimTypes.NameIdentifier);
var Name = identity.FindFirst("firstName");
var firstName = identity.FindFirst("firstName");
var Email = identity.FindFirst("Email");
var UserID = identity.FindFirst("UserID");
var claimsToKeep = new List<Claim> { tenantId, Name,firstName, Email, UserID };
return new ClaimsPrincipal(new ClaimsIdentity(claimsToKeep, identity.AuthenticationType, ClaimTypes.NameIdentifier, ClaimTypes.Role)
{
BootstrapContext = ((ClaimsIdentity)claimsPrincipal.Identity).BootstrapContext
});
}
I am using this method for Reset Password
public ResetTokenResult DoPasswordResetTokenForChange(string userId, string token)
{
switch (UserManager.FindById(userId))
{
case null:
return ResetTokenResult.UnknownUserId;
case CatalystUser user when ! (user.PasswordInvalidatedByReset ?? false):
return ResetTokenResult.TokenIsExpired;
case CatalystUser user when ! ((user.PasswordResetTokenExpiration ?? DateTime.MinValue) > DateTime.UtcNow):
return ResetTokenResult.TokenIsExpired;
case CatalystUser user when UserManager.VerifyUserToken(user.Id, "ResetPassword", token):
user.PasswordResetTokenExpiration = DateTime.UtcNow.AddDays(-1); // 1-time use. Invalidate now.UserManager.Update(user);
return ResetTokenResult.Success;
default:
return ResetTokenResult.InvalidToken;
}
}
Controller which I am using this method
[RequireHttpsWhenConfigured]
public async Task<ActionResult> Index(PasswordChangePage currentPage,
string userId, string token, string returnUrl = "")
{
var model = new PasswordChangePageViewModel(currentPage);
var isResetPasswordRequest = !string.IsNullOrEmpty(userId) && !string.IsNullOrEmpty(token);
if (!isResetPasswordRequest)
{
if (!RequestContext.IsCurrentUserAuthorized())
return Redirect(NavigationService.GetLoginLink());
model.PasswordChangeModel = new PasswordChangeViewModel {ReturnUrl = returnUrl};
model.ReturnUrl = returnUrl;
return View("Index", model);
}
if (RequestContext.IsCurrentUserAuthorized())
{
SignInManager.AuthenticationManager.SignOut();
return Redirect(Request.Url?.AbsoluteUri ?? "~/");
}
var loginLink = NavigationService.GetLoginLink();
var result = UserAccountService.DoPasswordResetTokenForChange(userId,Base64ForUrlDecode(token));
if ((result & ResetTokenResult.Failure) != ResetTokenResult.None)
{
model.ChangeCanProceed = false;
model.ErrorMessage = GetMessageForTokenResult(result);
model.LoginLink = loginLink;
}
else
{
model.PasswordChangeModel = new PasswordChangeViewModel { CurrentPassword = "null", IsResetPassword = true, UserId = userId, ResetPasswordToken = token };
model.ReturnUrl = loginLink;
}
return View("Index", model);
}
When users want to reset their password, they receive an email with a token link and everything works fine. As I know default ASPNET Identity token burns after 1 clicking to link.
My question is what is the best way to implement logic, the token link will burn after 5 clickings to link which is sent to email.
I create update user in my App and then
I test my app in Postman and in Web App but create different result.
When I tried this code in postman it work but web app doesn't work
(Code in ASP.NET CORE 2.0, Web App using Angular 5)
[HttpPut("{id}")]
public async Task<IActionResult> UpdateUser(int id, [FromBody] UserForUpdateDto userDto) {
if(!ModelState.IsValid)
return BadRequest(ModelState);
var currentUserId = int.Parse(User.FindFirst(ClaimTypes.NameIdentifier).Value);
var userFromRepo = await _orgRepo.GetUser(id);
if(userFromRepo == null)
return NotFound($"User not found with id: {id}");
if (currentUserId != userFromRepo.Id)
return Unauthorized();
_mapper.Map<UserForUpdateDto, User>(userDto, userFromRepo);
if (await _orgRepo.SaveAll())
return NoContent();
throw new Exception($"Updating user {id} failed on save");
}
From the WebApp it produce error:
"Object reference not set to an instance of an object."
When I debug the app it seems the line caused that
var currentUserId = int.Parse(User.FindFirst(ClaimTypes.NameIdentifier).Value);
I check and it produce null.
Any idea where the User was set ?
My Login Controller:
[HttpPost("login")]
public async Task<IActionResult> Login([FromBody]UserForLoginDto userForLoginDto)
{
var userFromRepo = await _repo.Login(userForLoginDto.Username.ToLower(), userForLoginDto.Password);
if (userFromRepo == null)
return Unauthorized();
// generate token
var tokenHandler = new JwtSecurityTokenHandler();
var key = Encoding.ASCII.GetBytes(_config.GetSection("AppSettings:Token").Value);
var tokenDescriptor = new SecurityTokenDescriptor
{
Subject = new ClaimsIdentity(new Claim[]
{
new Claim(ClaimTypes.NameIdentifier, userFromRepo.Id.ToString()),
new Claim(ClaimTypes.Name, userFromRepo.Username),
new Claim(ClaimTypes.Role, "RegisteredUsers")
}),
Expires = DateTime.Now.AddDays(3),
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key),
SecurityAlgorithms.HmacSha512Signature)
};
var token = tokenHandler.CreateToken(tokenDescriptor);
var tokenString = tokenHandler.WriteToken(token);
var user = _mapper.Map<UserForDetailDto>(userFromRepo);
return Ok(new { tokenString, user });
}
If an api method contains [Authorize] then an authorization header is sent along with the request. If no header is sent then you have no user.
[HttpPut("{id}")]
[Authorize(AuthenticationSchemes = "Bearer")]
public async Task<IActionResult> UpdateUser(int id, [FromBody] UserForUpdateDto userDto)
{
var sub = User.GetSubjectId(); // Subject Id is the user id
}
I configure a Web App that use AD FS, for this I use OWIN.
For the login, all is ok. If i'm an user of a domain and go to the website, he is automatically connected.
But what I want to have is to handle users and roles by myself after login.
So I want to check that an user exists in my database with this AD account (this process will be make before the login in another application)
I want to use Identity from Microsoft to handle claims (roles and permissions). But I don't understand how to put my code to handle the successfull connection from AD FS (with Ws-Federation) and add verification and fill in the right roles.
My code in ConfigureAuth:
public partial class Startup
{
private static string realm = ConfigurationManager.AppSettings["ida:Wtrealm"];
private static string adfsMetadata = ConfigurationManager.AppSettings["ida:ADFSMetadata"];
private NLogLoggingService _loggingService;
public void ConfigureAuth(IAppBuilder app)
{
_loggingService = new NLogLoggingService("Startup");
_loggingService.Debug("ConfigureAuth");
app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
app.UseCookieAuthentication(new CookieAuthenticationOptions());
app.UseWsFederationAuthentication(
new WsFederationAuthenticationOptions
{
Wtrealm = realm,
MetadataAddress = adfsMetadata,
//CallbackPath = PathString.FromUriComponent("/Account/TestCallback"),
// https://msdn.microsoft.com/en-us/library/microsoft.owin.security.authenticationmode(v=vs.113).aspx
AuthenticationMode = AuthenticationMode.Passive,
//Notifications = new WsFederationAuthenticationNotifications
//{
//}
});
}
In Web.config, realm is the link to my Web App (https://ssoadfs.test) and adfsMetadata is the link to metadata.xml from AD FS.
What is the way to go to set my role and login logic after AD FS connection ?
Schema that what I was thinking:
EDIT:
After some tries, I cannot handle any success callback. I don't want to have to handle roles in HomeController ...
My last Auth config:
_loggingService = new NLogLoggingService("Startup");
_loggingService.Debug("ConfigureAuth");
// Configure the db context, user manager and signin manager to use a single instance per request
app.CreatePerOwinContext(ApplicationUser.ApplicationDbContext.Create);
app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
app.CreatePerOwinContext<ApplicationSignInManager>(ApplicationSignInManager.Create);
app.SetDefaultSignInAsAuthenticationType(DefaultAuthenticationTypes.ApplicationCookie);
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
Provider = new CookieAuthenticationProvider
{
OnResponseSignIn = ctx =>
{
_loggingService.Debug("OnResponseSignIn");
ctx.Identity = TransformClaims(ctx, app);
},
// Enables the application to validate the security stamp when the user logs in.
// This is a security feature which is used when you change a password or add an external login to your account.
OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
validateInterval: TimeSpan.FromMinutes(30),
regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager))
}
});
app.UseWsFederationAuthentication(
new WsFederationAuthenticationOptions
{
Wtrealm = realm,
MetadataAddress = adfsMetadata,
Caption = "Active Directory",
CallbackPath = PathString.FromUriComponent("/Account/TestCallback"),
Notifications = new WsFederationAuthenticationNotifications
{
SecurityTokenValidated = n =>
{
new NLogLoggingService("Startup").Debug("SecurityTokenValidated");
var incomingClaimsFromAdfs = n.AuthenticationTicket.Identity.Claims.ToList();
var incomingClaimsHasNameIdentifier =
incomingClaimsFromAdfs.Any(
c => c.Type == System.Security.Claims.ClaimTypes.NameIdentifier);
_loggingService.Debug("SecurityTokenValidated - incomingClaimsHasNameIdentifier: " +
incomingClaimsHasNameIdentifier);
if (!incomingClaimsHasNameIdentifier)
{
var emailClaim =
incomingClaimsFromAdfs.First(c => c.Type == System.Security.Claims.ClaimTypes.Name);
_loggingService.Debug(emailClaim.Value);
}
//if (!incomingClaimsHasNameIdentifier)
//{
// var emailClaim = incomingClaimsFromAdfs.First(c => c.Type == System.Security.Claims.ClaimTypes.Name);
// incomingClaimsFromAdfs.Add();
// IUser user = await this.UserStore.FindByNameOrEmailAsync(userNameOrEmailAddress);
// if ((Entity<long>)user == (Entity<long>)null)
// LoginResult = new ApplicationUserManager.LoginResult(LoginResultType.InvalidUserNameOrEmailAddress, default(IUser));
// //else if (!loggedInFromExternalSource && new PasswordHasher().VerifyHashedPassword(user.Password, plainPassword) != PasswordVerificationResult.Success)
// // LoginResult = new UserManager<TTenant, TRole, TUser>.LoginResult(LoginResultType.InvalidPassword, user);
// else
// LoginResult = await this.CreateLoginResultAsync(user, tenant);
//}
//else
//{
// throw new ApplicationException("Get ADFS to provide the NameIdentifier claim!");
//}
//var normalizedClaims = incomingClaimsFromAdfs.Distinct(new ClaimComparer());
//var claimsIdentity = new ClaimsIdentity(normalizedClaims, n.AuthenticationTicket.Identity.AuthenticationType);
//n.AuthenticationTicket = new AuthenticationTicket(claimsIdentity, n.AuthenticationTicket.Properties);
return Task.FromResult(0);
}
}
});
In this code, I tried CallbackPath (nothing appeared in my log), WsFederationAuthenticationNotifications.SecurityTokenValidated (nothing appeared in my log), CookieAuthenticationProvider.OnResponseSignIn (same nothing happened)
In HomeController i'm able to have Identity.Name:
public ActionResult Index()
{
if (HttpContext.GetOwinContext().Authentication.User.Identity.IsAuthenticated)
{
new NLogLoggingService("Home").Debug("User is authenticated");
}
return View();
}
Did I miss something to get Notifications working or Provider in CookieAuthenticationOptions ???
If you use ASP.NET Identity 2.0 or later version, you can use an approach similar to it shown below. Please note that this approach assign GroupRoles to the user instead of assigning each of roles one by one. You can change necessary parts according to your needs.
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
{
if (!ModelState.IsValid)
{
return View(model);
}
ApplicationGroupManager groupManager = new ApplicationGroupManager();
if (Membership.ValidateUser(model.UserName, model.Password))
{
FormsAuthentication.SetAuthCookie(model.UserName, model.RememberMe);
//Assign Roles to the current User
ApplicationUser user = UserManager.FindByName(model.UserName);
//If the user is registered in the system (ASP.NET Identity) add record to AspNetUsers table
if (user != null)
{
//Returns Group Id and Role Id by using User Id parameter
var userGroupRoles = groupManager.GetUserGroupRoles("bfd9730e-2093-4fa0-89a2-226e301d831b");
foreach (var role in userGroupRoles)
{
string roleName = RoleManager.FindById(role.ApplicationRoleId).Name;
UserManager.AddToRole(user.Id, roleName);
}
}
else
{
//crate new user
//first retrieve user info from LDAP:
// Create an array of properties that we would like and add them to the search object
string[] requiredProperties = new string[] { "samaccountname", "givenname", "sn", "mail", "physicalDeliveryOfficeName", "title" };
var userInfo = CreateDirectoryEntry(model.UserName, requiredProperties);
var newUser = new ApplicationUser();
newUser.UserName = userInfo.GetDirectoryEntry().Properties["samaccountname"].Value.ToString();
newUser.Name = userInfo.GetDirectoryEntry().Properties["givenname"].Value.ToString();
newUser.Surname = userInfo.GetDirectoryEntry().Properties["sn"].Value.ToString();
newUser.Email = userInfo.GetDirectoryEntry().Properties["mail"].Value.ToString();
newUser.EmailConfirmed = true;
newUser.PasswordHash = null;
var result = await UserManager.CreateAsync(newUser);
if (result.Succeeded)
{
//If the user is created ...
}
//Assign user group (and roles)
var defaultGroup = "751b30d7-80be-4b3e-bfdb-3ff8c13be05e";
groupManager.SetUserGroups(newUser.Id, new string[] { defaultGroup });
}
return this.RedirectToAction("Index", "Issue");
}
this.ModelState.AddModelError(string.Empty, "Wrong username or password!");
return this.View(model);
}
static SearchResult CreateDirectoryEntry(string sAMAccountName, string[] requiredProperties)
{
DirectoryEntry ldapConnection = null;
try
{
ldapConnection = new DirectoryEntry("LDAP://OU=******, DC=******, DC=******", "acb#xyz.com", "YourPassword");
ldapConnection.AuthenticationType = AuthenticationTypes.Secure;
DirectorySearcher search = new DirectorySearcher(ldapConnection);
search.Filter = String.Format("(sAMAccountName={0})", sAMAccountName);
foreach (String property in requiredProperties)
search.PropertiesToLoad.Add(property);
SearchResult result = search.FindOne();
//SearchResultCollection searchResultCollection = search.FindAll(); //You can also retrieve all information
if (result != null)
{
return result;
}
else {
return null;
//Console.WriteLine("User not found!");
}
}
catch (Exception e)
{
Console.WriteLine("Exception caught:\n\n" + e.ToString());
}
return null;
}
In the last few days I have been working on integrating Umbraco Backoffice with IdentityServer v3. I have managed to get to the point, where I authenticate user externally and have Umbraco create a user with some default user type in the backoffice and link it to the external account.
The next thing I'm doing is updating the Umbraco user type, based on the roles of the user. I think I found a way of doing that on linking the Umbraco to the external account, but I cannot see any way to constantly update the user types with each login, in case the roles were removed/added for a user.
By analyzing the code in Umbraco BackOfficeController, it seems there is no way to get into the process of authenticating and update data on the side of Umbraco.
var user = await UserManager.FindAsync(loginInfo.Login);
if (user != null)
{
await SignInManager.SignInAsync(user, isPersistent: false, rememberBrowser: false);
}
else
{
if (await AutoLinkAndSignInExternalAccount(loginInfo) == false)
{
ViewBag.ExternalSignInError = new[] { "The requested provider (" + loginInfo.Login.LoginProvider + ") has not been linked to to an account" };
}
}
It seems that if the umbraco login is found, then the user is just being logged in, without any exposed events or options. Only if the user is not found, then the whole process of creation and linking is started, where I could actually make some changes to the user properties.
That said, is there any way to actually update the user types of an Umbraco user, based on the claims from external server, on every login?
My code from the Startup class is below.
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie
});
var idAuth = new OpenIdConnectAuthenticationOptions
{
Authority = "https://localhost:44332",
ClientId = "id",
ClientSecret = "secret",
RedirectUri = "http://localhost:8081/Umbraco",
ResponseType = "id_token token",
Scope = "openid profile roles email",
Caption = "test",
SignInAsAuthenticationType = Umbraco.Core.Constants.Security.BackOfficeExternalAuthenticationType
};
idAuth.Notifications = new OpenIdConnectAuthenticationNotifications
{
SecurityTokenValidated = async n =>
{
var id = n.AuthenticationTicket.Identity;
var givenName = id.FindFirst(System.Security.Claims.ClaimTypes.GivenName);
var familyName = id.FindFirst(System.Security.Claims.ClaimTypes.Surname);
var roles = id.FindAll(System.Security.Claims.ClaimTypes.Role);
var nid = new ClaimsIdentity(
id.AuthenticationType,
System.Security.Claims.ClaimTypes.GivenName,
System.Security.Claims.ClaimTypes.Role);
nid.AddClaim(givenName);
nid.AddClaim(familyName);
nid.AddClaims(roles);
nid.AddClaim(id.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier));
nid.AddClaim(id.FindFirst(System.Security.Claims.ClaimTypes.Email));
n.AuthenticationTicket = new AuthenticationTicket(
nid,
n.AuthenticationTicket.Properties);
}
};
//idAuth.AuthenticationType = "https://localhost:44332";
idAuth.ForUmbracoBackOffice("btn-google-plus", "fa-google-plus"); //temporary icon/button
idAuth.AuthenticationType = "https://localhost:44332";
var externalOptions = new ExternalSignInAutoLinkOptions(autoLinkExternalAccount: true, defaultUserType: "admin");
//externalOptions.OnAutoLinking; // TODO: set user type based on roles
idAuth.SetExternalSignInAutoLinkOptions(externalOptions);
app.UseOpenIdConnectAuthentication(idAuth);
Managed to solve this some time ago by manually checking the roles claim and Umbraco UserType on SecurityTokenValidated with the help of Umbraco services IExternalLoginService and IUserService. If the combination is not right (e.g. the administrator role is not present in the claim), I use Umbraco IUserService to update that user's UserType
Notifications =
new OpenIdConnectAuthenticationNotifications
{
SecurityTokenValidated = async n =>
{
var id = n.AuthenticationTicket.Identity;
var uid = id.FindFirst(ClaimTypes.NameIdentifier);
var givenName = id.FindFirst(ClaimTypes.GivenName);
var familyName = id.FindFirst(ClaimTypes.Surname);
var roles = id.FindAll(ClaimTypes.Role);
var rolesList = roles as IList<Claim> ?? roles.ToList();
if (
!rolesList.Any(
c =>
string.Equals(c.Value, RoleNames.ContentEditor,
StringComparison.InvariantCultureIgnoreCase)))
throw new HttpException(403,
"You do not have any roles configured for the application");
// create new identity and set name and role claim type
var nid = new ClaimsIdentity(
id.AuthenticationType,
ClaimTypes.GivenName,
ClaimTypes.Role);
UpdateUserType(uid.Value, rolesList, applicationConfiguration.AuthorityUrl);
nid.AddClaim(givenName);
nid.AddClaim(familyName);
nid.AddClaims(rolesList);
nid.AddClaim(uid);
nid.AddClaim(id.FindFirst(ClaimTypes.Email));
n.AuthenticationTicket = new AuthenticationTicket(
nid,
n.AuthenticationTicket.Properties);
}
}
private static void UpdateUserType(string uid, IList<Claim> roles, string providerName)
{
var userService = ApplicationContext.Current.Services.UserService;
var oneUser = ApplicationContext.Current.Services.ExternalLoginService.Find(new UserLoginInfo(providerName, uid)).FirstOrDefault();
if (oneUser == null)
return;
var user = userService.GetUserById(oneUser.UserId);
if (user == null)
return;
if (
roles.Any(
r => string.Equals(r.Value, RoleNames.Administrator, StringComparison.InvariantCultureIgnoreCase))
&& !string.Equals(user.UserType.Alias, UmbracoRoleNames.Administrator))
{
SetUserType(user, UmbracoRoleNames.Administrator, userService);
return;
}
if (
roles.Any(
r => string.Equals(r.Value, RoleNames.ContentEditor, StringComparison.InvariantCultureIgnoreCase))
&& !string.Equals(user.UserType.Alias, UmbracoRoleNames.ContentEditor))
{
SetUserType(user, UmbracoRoleNames.ContentEditor, userService);
return;
}
}
private static void SetUserType(Umbraco.Core.Models.Membership.IUser user, string alias, IUserService userService)
{
try
{
user.UserType = userService.GetUserTypeByAlias(alias);
userService.Save(user);
}
catch (Exception e)
{
LogHelper.Error(typeof(ClassName), "Could not update the UserType of a user.", e);
}
}
In this specific case, I do not change the UserType back to a non-admin/non-content editor one when someone lacks that privilege from their roles claim, because they are being filtered out one step before and a 403 error code is being returned.