I am creating a default user for my system. However I am creating through SQL Server database.
I use ASP.NET Core and Entity Framework to handle logins, by default Entity Framework creates a table called AspNetUsers; this table has a column called PasswordHash, I believe the encryption used is of the Hash type.
I am entering the password on this user by the database as follows:
DECLARE #HashThis nvarchar(4000);
SET #HashThis = CONVERT(nvarchar(4000),'Administrador');
SELECT HASHBYTES('SHA1', #HashThis)
UPDATE AspNetUsers
SET PasswordHash = HASHBYTES('SHA1', #HashThis),
SecurityStamp = '0b12450e-016d-4cd6-af7b-fa6d2198586f',
ConcurrencyStamp = 'a63a5236-4020-4f69-93b1-9f077ba014cd',
UserName = 'administrador#administrador.com.br'
But the password column is getting strange characters in Japanese, it follows the image:
The biggest issue and when I log in ASP.NET Core, only the password invalidates.
How can I do to bypass this?
Observation: when I create the user through ASP.NET Core, it works normally.
Here is one example how you can seed you user:
In you SecurityDbContext you can create following methods (I added SeedRoles in case you need them):
public static async Task Seed(IServiceProvider serviceProvider)
{
await SeedRoles(serviceProvider);
await SeedUsers(serviceProvider);
}
Seed Roles:
public static async Task SeedRoles(IServiceProvider serviceProvider)
{
RoleManager<ApplicationRole> roleManager = serviceProvider.GetRequiredService<RoleManager<ApplicationRole>>();
string[] roles = ...;
foreach(var role in roles)
{
ApplicationRole appRole = await roleManager.FindByNameAsync(role);
if (appRole == null)
{
await roleManager.CreateAsync(new ApplicationRole(role));
}
}
}
Seed User:
public static async Task SeedUser(IServiceProvider serviceProvider, UserManager<ApplicationUser> userManager, string email, string password, string roleName = "")
{
string userName = roleName;
ApplicationUser user = await userManager.FindByNameAsync(userName);
if (user == null)
{
// Create user account if it doesn't exist
user = new ApplicationUser
{
UserName = userName,
Email = email
};
IdentityResult result = await userManager.CreateAsync(user, password);
// Assign role to the user
if (result.Succeeded)
{
user = await userManager.FindByNameAsync(userName);
}
}
if (user != null && roleName.Length > 0)
{
await userManager.AddToRoleAsync(user, roleName);
}
}
From SeedUsers method, just call SeedUser as many times as you need.
And then just simply call Seed method from Startup.cs Configure method:
SecurityDbContextSeed.Seed(app.ApplicationServices).Wait();
Related
Questions
First question, what determines if an sid claim is emitted from identityserver?
Second question, do I even need an sid? I currently have it included because it was in the sample..
Backstory
I have one website that uses IdentityServer4 for authentication and one website that doesn't. I've cobbled together a solution that allows a user to log into the non-identityserver4 site and click a link that uses one-time-access codes to automatically log into the identityserver4 site. Everything appears to work except the sid claim isn't passed along from identityserver to the site secured by identityserver when transiting from the non-identityserver site. If I log directly into the identityserver4 secured site the sid is included in the claims. Code is adapted from examples of automatically logging in after registration and/or impersonation work flows.
Here is the code:
One time code login process in identityserver4
public class CustomAuthorizeInteractionResponseGenerator : AuthorizeInteractionResponseGenerator
{
...
//https://stackoverflow.com/a/51466043/391994
public override async Task<InteractionResponse> ProcessInteractionAsync(ValidatedAuthorizeRequest request,
ConsentResponse consent = null)
{
string oneTimeAccessToken = request.GetAcrValues().FirstOrDefault(x => x.Split(':')[0] == "otac");
string clientId = request.ClientId;
//handle auto login handoff
if (!string.IsNullOrWhiteSpace(oneTimeAccessToken))
{
//https://benfoster.io/blog/identity-server-post-registration-sign-in/
oneTimeAccessToken = oneTimeAccessToken.Split(':')[1];
OneTimeCodeContract details = await GetOTACFromDatabase(oneTimeAccessToken);
if (details.IsValid)
{
UserFormContract user = await GetPersonUserFromDatabase(details.PersonId);
if (user != null)
{
string subjectId = await GetClientSubjectIdAsync(clientId, user.AdUsername);
var iduser = new IdentityServerUser(subjectId)
{
DisplayName = user.AdUsername,
AuthenticationTime = DateTime.Now,
IdentityProvider = "local",
};
request.Subject = iduser.CreatePrincipal();
//revoke token
bool? success = await InvalidateTokenInDatabase(oneTimeAccessToken);
if (success.HasValue && !success.Value)
{
Log.Debug($"Revoke failed for {oneTimeAccessToken} it should expire at {details.ExpirationDate}");
}
//https://stackoverflow.com/a/56237859/391994
//sign them in
await _httpContextAccessor.HttpContext.SignInAsync(IdentityServerConstants.DefaultCookieAuthenticationScheme, request.Subject, null);
return new InteractionResponse
{
IsLogin = false,
IsConsent = false,
};
}
}
}
return await base.ProcessInteractionAsync(request, consent);
}
}
Normal Login flow when logging directly into identityserver4 secured site (from sample)
public class AccountController : Controller
{
/// <summary>
/// Handle postback from username/password login
/// </summary>
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Login(LoginInputModel model)
{
Log.Information($"login request from: {Request.HttpContext.Connection.RemoteIpAddress.ToString()}");
if (ModelState.IsValid)
{
// validate username/password against in-memory store
if (await _userRepository.ValidateCredentialsAsync(model.Username, model.Password))
{
AuthenticationProperties props = null;
// only set explicit expiration here if persistent.
// otherwise we reply upon expiration configured in cookie middleware.
if (AccountOptions.AllowRememberLogin && model.RememberLogin)
{
props = new AuthenticationProperties
{
IsPersistent = true,
ExpiresUtc = DateTimeOffset.UtcNow.Add(AccountOptions.RememberMeLoginDuration)
};
};
var clientId = await _account.GetClientIdAsync(model.ReturnUrl);
// issue authentication cookie with subject ID and username
var user = await _userRepository.FindByUsernameAsync(model.Username, clientId);
var iduser = new IdentityServerUser(user.SubjectId)
{
DisplayName = user.UserName
};
await HttpContext.SignInAsync(iduser, props);
// make sure the returnUrl is still valid, and if yes - redirect back to authorize endpoint
if (_interaction.IsValidReturnUrl(model.ReturnUrl))
{
return Redirect(model.ReturnUrl);
}
return Redirect("~/");
}
ModelState.AddModelError("", AccountOptions.InvalidCredentialsErrorMessage);
}
// something went wrong, show form with error
var vm = await _account.BuildLoginViewModelAsync(model);
return View(vm);
}
}
AuthorizationCodeReceived in identityserver4 secured site
public partial class Startup
{
public void Configuration(IAppBuilder app)
{
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthorizationCodeReceived = async n =>
{
// use the code to get the access and refresh token
var tokenClient = new TokenClient(
tokenEndpoint,
electionClientId,
electionClientSecret);
var tokenResponse = await tokenClient.RequestAuthorizationCodeAsync(
n.Code, n.RedirectUri);
if (tokenResponse.IsError)
{
throw new Exception(tokenResponse.Error);
}
// use the access token to retrieve claims from userinfo
var userInfoClient = new UserInfoClient(
new Uri(userInfoEndpoint).ToString());
var userInfoResponse = await userInfoClient.GetAsync(tokenResponse.AccessToken);
Claim subject = userInfoResponse.Claims.Where(x => x.Type == "sub").FirstOrDefault();
// create new identity
var id = new ClaimsIdentity(n.AuthenticationTicket.Identity.AuthenticationType);
id.AddClaims(GetRoles(subject.Value, tokenClient, apiResourceScope, apiBasePath));
var transformedClaims = StartupHelper.TransformClaims(userInfoResponse.Claims);
id.AddClaims(transformedClaims);
id.AddClaim(new Claim("access_token", tokenResponse.AccessToken));
id.AddClaim(new Claim("expires_at", DateTime.Now.AddSeconds(tokenResponse.ExpiresIn).ToLocalTime().ToString()));
id.AddClaim(new Claim("refresh_token", tokenResponse.RefreshToken));
id.AddClaim(new Claim("id_token", n.ProtocolMessage.IdToken));
THIS FAILS -> id.AddClaim(new Claim("sid", n.AuthenticationTicket.Identity.FindFirst("sid").Value));
n.AuthenticationTicket = new AuthenticationTicket(
new ClaimsIdentity(id.Claims, n.AuthenticationTicket.Identity.AuthenticationType, "name", "role"),
n.AuthenticationTicket.Properties);
},
}
});
}
}
Questions again if you don't want to scroll back up
First question, what determines if an sid claim is emitted from identityserver?
Second question, do I even need an sid? I currently have it included because it was in the sample..
I have a user object which saved to the cloud firestore database when the user Sign In / Sign Up.
So the user object is retrieved from the database when he signs in, and everything works until i try to do the 'add' operation on the list 'usersProject':
// Add the new project ID to the user's project list
user.userProjectsIDs.add(projectID);
So i get the exception Unhandled Exception: Unsupported operation: Cannot add to a fixed-length list
i believe the problem is when converting the user from json to object, because when the user is signing up the object is converted to json and stored in the database and the user will be automatically signed in using the object before converting.
void createUser(String email, String password, String username, String name, String birthDate) async {
try {
// Check first if username is taken
bool usernameIsTaken = await UserProfileCollection()
.checkIfUsernameIsTaken(username.toLowerCase().trim());
if (usernameIsTaken) throw FormatException("Username is taken");
// Create the user in the Authentication first
final firebaseUser = await _auth.createUserWithEmailAndPassword(
email: email.trim(), password: password.trim());
// Encrypting the password
String hashedPassword = Password.hash(password.trim(), new PBKDF2());
// Create new list of project for the user
List<String> userProjects = new List<String>();
// Create new list of friends for the user
List<String> friends = new List<String>();
// Creating user object and assigning the parameters
User _user = new User(
userID: firebaseUser.uid,
userName: username.toLowerCase().trim(),
email: email.trim(),
password: hashedPassword,
name: name,
birthDate: birthDate.trim(),
userAvatar: '',
userProjectsIDs: userProjects,
friendsIDs: friends,
);
// Create a new user in the fire store database
await UserProfileCollection().createNewUser(_user);
// Assigning the user controller to the 'user' object
Get.find<UserController>().user = _user;
Get.back();
} catch (e) {
print(e.toString());
}}
When the user is signed off then he signs in and try to make operation on the user object, here comes the problem some of the properties (the List type) can't be used.
This code creates project and add projectID to the user's list
Future<void> createNewProject(String projectName, User user) async {
String projectID = Uuid().v1(); // Project ID, UuiD is package that generates random ID
// Add the creator of the project to the members list and assign him as admin
var member = Member(
memberUID: user.userID,
isAdmin: true,
);
List<Member> membersList = new List();
membersList.add(member);
// Save his ID in the membersUIDs list
List <String> membersIDs = new List();
membersIDs.add(user.userID);
// Create chat for the new project
var chat = Chat(chatID: projectID);
// Create the project object
var newProject = Project(
projectID: projectID,
projectName: projectName,
image: '',
joiningLink: '$projectID',
isJoiningLinkEnabled: true,
pinnedMessage: '',
chat: chat,
members: membersList,
membersIDs: membersIDs,
);
// Add the new project ID to the user's project list
user.userProjectsIDs.add(projectID);
try {
// Convert the project object to be a JSON
var jsonUser = user.toJson();
// Send the user JSON data to the fire base
await Firestore.instance
.collection('userProfile')
.document(user.userID)
.setData(jsonUser);
// Convert the project object to be a JSON
var jsonProject = newProject.toJson();
// Send the project JSON data to the fire base
return await Firestore.instance
.collection('projects')
.document(projectID)
.setData(jsonProject);
} catch (e) {
print(e);
}}
Here where the exception happens only when the user signs off then signs in, but when he signed up for the first time there will be no exception.
// Add the new project ID to the user's project list
user.userProjectsIDs.add(projectID);
The sign in function
void signIn(String email, String password) async {
try {
// Signing in
FirebaseUser firebaseUser = await _auth.signInWithEmailAndPassword(email: email.trim(), password: password.trim());
// Getting user document form firebase
DocumentSnapshot userDoc = await UserProfileCollection().getUser(firebaseUser.uid);
// Converting the json data to user object and assign the user object to the controller
Get.find<UserController>().user = User.fromJson(userDoc.data);
print(Get.find<UserController>().user.userName);
} catch (e) {
print(e.toString());
}}
I think the problem caused by User.fromJson
why it makes the array from the firestore un-modifiable ?
The user class
class User {
String userID;
String userName;
String email;
String password;
String name;
String birthDate;
String userAvatar;
List<String> userProjectsIDs;
List<String> friendsIDs;
User(
{this.userID,
this.userName,
this.email,
this.password,
this.name,
this.birthDate,
this.userAvatar,
this.userProjectsIDs,
this.friendsIDs});
User.fromJson(Map<String, dynamic> json) {
userID = json['userID'];
userName = json['userName'];
email = json['email'];
password = json['password'];
name = json['name'];
birthDate = json['birthDate'];
userAvatar = json['UserAvatar'];
userProjectsIDs = json['userProjectsIDs'].cast<String>();
friendsIDs = json['friendsIDs'].cast<String>();
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
data['userID'] = this.userID;
data['userName'] = this.userName;
data['email'] = this.email;
data['password'] = this.password;
data['name'] = this.name;
data['birthDate'] = this.birthDate;
data['UserAvatar'] = this.userAvatar;
data['userProjectsIDs'] = this.userProjectsIDs;
data['friendsIDs'] = this.friendsIDs;
return data;
}
}
Just add the growable argument..
If [growable] is false, which is the default, the list is a fixed-length list of length zero. If [growable] is true, the list is growable and equivalent to [].
final growableList = List.empty(growable: true);
this works for me:
list = list.toList();
list.add(value);
Your JSON decoding is likely returning fixed-length lists, which you're then using to initialize userProjectsIDs in the User class. This prevent you from adding additional elements.
Change the following from the fromJson constructor:
userProjectsIDs = json['userProjectsIDs'].cast<String>();
to
userProjectsIDs = List.of(json['userProjectsIDs'].cast<String>());
I have my Initialiser setup and everything seems to run correctly and all the details are saved to the database but when I try to log in it via the webapp it fails everytime. When I run the debugger in the login controller it returns {Failed} after this is hit:
var result = await _signInManager.PasswordSignInAsync(Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: false);
Initialiser:
public class DbInitialiser : IDbInitialiser
{
private readonly ApplicationDbContext _db;
private readonly UserManager<IdentityUser> _userManager;
private readonly RoleManager<IdentityRole> _roleManager;
public DbInitialiser(UserManager<IdentityUser> userManager, RoleManager<IdentityRole> roleManager, ApplicationDbContext db)
{
_db = db;
_userManager = userManager;
_roleManager = roleManager;
}
public void Initialise()
{
try
{
if (_db.Database.GetPendingMigrations().Count() > 0)
{
_db.Database.Migrate();
}
}
catch (Exception ex)
{
}
if (_db.Roles.Any(r => r.Name == "Admin")) return;
_roleManager.CreateAsync(new IdentityRole("Admin")).GetAwaiter().GetResult();//makes sure this executes before proceceding with anything else
_roleManager.CreateAsync(new IdentityRole("Manager")).GetAwaiter().GetResult();
_userManager.CreateAsync(new Employee
{
UserName = "Admin",
Email = "admin#gmail.com",
EmailConfirmed = true,
TwoFactorEnabled = false,
PhoneNumberConfirmed = true
//can set other properties, this is for the initial setup
}, "Abc123!Abc123!").GetAwaiter().GetResult();
IdentityUser user = _db.Users.Where(u => u.Email == "admin#gmail.com").FirstOrDefault();
_userManager.AddToRoleAsync(user, "Admin").GetAwaiter().GetResult();
}
(My Employee class extends IdentityUser)
I have checked all my password requirements as mentioned in other similar posts so I know it isnt to do with that and when I check in SSMS all the data for the user is there in aspnetusers so I am not sure why it wont let me login to the admin user that is seeded
When I run the debugger in the login controller it returns {Failed}
In the source code of SignInManager<TUser>.PasswordSignInAsync method, we can find it would check the user based on the provided userName.
public virtual async Task<SignInResult> PasswordSignInAsync(string userName, string password,
bool isPersistent, bool lockoutOnFailure)
{
var user = await UserManager.FindByNameAsync(userName);
if (user == null)
{
return SignInResult.Failed;
}
return await PasswordSignInAsync(user, password, isPersistent, lockoutOnFailure);
}
In your code, you set UserName with "Admin" that is not same as Email with "admin#gmail.com". If user login with email account, the code snippet var user = await UserManager.FindByNameAsync(userName); would return null, which cause the issue.
To fix it, you can try to set UserName with same value of Email (admin#gmail.com). Or modify the login code logic to find user by the Email, then sign in that user with password, like below.
public async Task<IActionResult> OnPostAsync(string returnUrl = null)
{
returnUrl = returnUrl ?? Url.Content("~/");
if (ModelState.IsValid)
{
// This doesn't count login failures towards account lockout
// To enable password failures to trigger account lockout, set lockoutOnFailure: true
var user = await _userManager.FindByEmailAsync(Input.Email);
//var istrue = await _userManager.CheckPasswordAsync(user, Input.Password);
var result = await _signInManager.PasswordSignInAsync(user, Input.Password, Input.RememberMe, lockoutOnFailure: true);
//var result = await _signInManager.PasswordSignInAsync(Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: true);
I have an application that currently uses the resource owner password grant type to allow users to log in via a single page application. The identity server is hosted in the same project as the Web API currently. However, we would like to add the ability for a user to register / log in using their Google account. Currently, the user data is stored in tables and managed by ASP.NET Core Identity.
Is there a way to have both the resource owner password grant type available in the application for users who are 'local' but also enable third party authentication via Google? Currently, we hit the Identity Server token endpoint with a username and password and store the token in the browser. It's then passed to any endpoint that requires authorization. Would this same flow still work when integrating Google authentication and retrieving the Google token?
All the credit goes to Behrooz Dalvandi for this amazing post.
The solution to this problem is to create a custom grant and implement IExtensionGrantValidator.
public class GoogleGrant : IExtensionGrantValidator
{
private readonly IGoogleService _googleService;
private readonly IAccountService _accountService;
public GoogleGrant(IGoogleService googleService, IAccountService accountService)
{
_googleService = googleService;
_accountService = accountService;
}
public string GrantType
{
get
{
return "google_auth";
}
}
public async Task ValidateAsync(ExtensionGrantValidationContext context)
{
var userToken = context.Request.Raw.Get("id_token");
if (string.IsNullOrEmpty(userToken))
{
//You may want to add some claims here.
context.Result = new GrantValidationResult(TokenErrors.InvalidGrant, null);
return;
}
//Validate ID token
GoogleJsonWebSignature.Payload idTokenData = await _googleService.ParseGoogleIdToken(userToken);
if (idTokenData != null)
{
//Get user from the database.
ApplicationUser user = await _accountService.FindByEmail(idTokenData.Email);
if(user != null)
{
context.Result = new GrantValidationResult(user.Id, "google");
return;
}
else
{
return;
}
}
else
{
return;
}
}
}
Configure this validator in the Startup
var builder = services.AddIdentityServer()
.AddInMemoryIdentityResources(Config.Ids)
.AddInMemoryApiResources(Config.Apis)
.AddInMemoryClients(Config.Clients)
.AddResourceOwnerValidator<CustomResourceOwnerPasswordValidator>()
.AddExtensionGrantValidator<GoogleGrant>();//Custom validator.
And last but not the least.
Add below code in the config file. Notice the 'google_auth' grant type.
new Client
{
ClientId = "resourceownerclient",
AllowedGrantTypes = GrantTypes.ResourceOwnerPasswordAndClientCredentials.Append("google_auth").ToList(),
AccessTokenType = AccessTokenType.Jwt,
AccessTokenLifetime = 3600,
IdentityTokenLifetime = 3600,
UpdateAccessTokenClaimsOnRefresh = true,
SlidingRefreshTokenLifetime = 30,
AllowOfflineAccess = true,
RefreshTokenExpiration = TokenExpiration.Absolute,
RefreshTokenUsage = TokenUsage.OneTimeOnly,
AlwaysSendClientClaims = true,
Enabled = true,
ClientSecrets= new List<Secret> { new Secret("dataEventRecordsSecret".Sha256()) },
AllowedScopes = {
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.StandardScopes.Email,
IdentityServerConstants.StandardScopes.OfflineAccess,
"api1"
}
}
I'm using out-of-the-box auth with Individual User Accounts that comes with the Visual Studio template for Web Api. I consume the api in an Angular.js front end.
What is the 'canonical' way of providing user profile to the front end?
Are getting the token and getting user profile (email, first and last name, roles) separate activities or should /Token provide the token and at least the roles and maybe first and last name so the UI can display it?
I'm looking for a general guidance about architecture/flow for apps using a token for auth as well as ASP.Net Web Api + Angular.js specific info.
For the record this is how I implemented it.
TL;DR
I decided to use claims, because 'GivenName', 'Surname' already exists which suggests that it's an OK place to store this info.
I found it very awkward to edit claims.
Details
Here's my Add/UpdateUser method. I hate the way claims are handled, but I couldn't find a better way.
[HttpPost]
[Authorize(Roles = "admin")]
public async Task<IHttpActionResult> Post(AccountModelDTO model)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
using (var transaction = Request.GetOwinContext().Get<ApplicationDbContext>().Database.BeginTransaction())
{
ApplicationUser user;
if( string.IsNullOrEmpty(model.Id) )
{//Add user
user = new ApplicationUser() { UserName = model.Email, Email = model.Email };
IdentityResult resultAdd = await UserManager.CreateAsync(user); //Note, that CreateAsync this sets user.Id
if (!resultAdd.Succeeded)
{
return GetErrorResult(resultAdd);
}
} else
{//Update user
user = await UserManager.FindByIdAsync(model.Id);
if( user == null )
{
throw new HttpResponseException(Request.CreateResponse(System.Net.HttpStatusCode.BadRequest, "Unknown id"));
}
user.UserName = model.Email;
user.Email = model.Email;
//Remove existing claims
var claims = user.Claims.Where(c=>c.ClaimType == ClaimTypes.GivenName).ToList();
foreach( var claim in claims)
{
await UserManager.RemoveClaimAsync(user.Id, new Claim(ClaimTypes.GivenName, claim.ClaimValue));
}
claims = user.Claims.Where(c => c.ClaimType == ClaimTypes.Surname).ToList();
foreach (var claim in claims)
{
await UserManager.RemoveClaimAsync(user.Id, new Claim(ClaimTypes.Surname, claim.ClaimValue));
}
claims = user.Claims.Where(c => c.ClaimType == ClaimTypes.Role).ToList();
foreach (var claim in claims)
{
await UserManager.RemoveClaimAsync(user.Id, new Claim(ClaimTypes.Role, claim.ClaimValue));
}
}
var result = await UserManager.AddClaimAsync(user.Id, new Claim(ClaimTypes.GivenName, model.FirstName));
if (!result.Succeeded)
{
return GetErrorResult(result);
}
await UserManager.AddClaimAsync(user.Id, new Claim(ClaimTypes.Surname, model.LastName));
if (!result.Succeeded)
{
return GetErrorResult(result);
}
foreach (var role in model.Roles)
{
result = await UserManager.AddClaimAsync(user.Id, new Claim(ClaimTypes.Role, role));
}
if (!result.Succeeded)
{
return GetErrorResult(result);
}
transaction.Commit();
return Ok();
}
}