tutorial net español asp c# asp.net-core identityserver4

c# - español - asp.net core identity



IdentityServer4 registra UserService y obtiene usuarios de la base de datos en asp.net core (3)

He buscado todo sobre cómo registrar un UserService con IdentityServer4 en asp.net core, pero parece que no puedo encontrar la manera correcta de hacerlo.

Este es el código para registrar InMemoryUsers que se encuentra here , sin embargo, me gustaría acceder a los usuarios de mi base de datos MSSQL no usuarios estáticos definidos en la muestra.

var builder = services.AddIdentityServer(options => { options.SigningCertificate = cert; }); builder.AddInMemoryClients(Clients.Get()); builder.AddInMemoryScopes(Scopes.Get()); builder.AddInMemoryUsers(Users.Get());

Entonces miré this que es para IdentityServer3 .

var factory = new IdentityServerServiceFactory() .UseInMemoryClients(Clients.Get()) .UseInMemoryScopes(Scopes.Get()); var userService = new UserService(); factory.UserService = new Registration<IUserService>(resolver => userService);

Al leer en línea parece que necesito usar el sistema DI para registrar el UserService, pero no estoy seguro de cómo se vincula al IdentityServer, por ejemplo.

services.AddScoped<IUserService, UserService>();

Entonces mi pregunta es:

¿Cómo vinculo mi UserService al constructor (IdentityServer4 Users)? ¿Y cómo llamaría a mi base de datos para acceder y autenticar a mis usuarios de db existentes en el UserService (uso repositorios para conectarme a db)?

Teniendo en cuenta esto, tiene que funcionar con asp.net core .

¡Gracias!


En IdentityServer4 1.0.0-rc5 ni IUserService ni CustomGrantValidationResult están disponibles.

Ahora, en lugar de devolver un CustomGrantValidationResult, deberá establecer el contexto.

public class ResourceOwnerPasswordValidator: IResourceOwnerPasswordValidator { private MyUserManager _myUserManager { get; set; } public ResourceOwnerPasswordValidator() { _myUserManager = new MyUserManager(); } public async Task ValidateAsync(ResourceOwnerPasswordValidationContext context) { var user = await _myUserManager.FindByNameAsync(context.UserName); if (user != null && await _myUserManager.CheckPasswordAsync(user,context.Password)) { context.Result = new GrantValidationResult( subject: "2", authenticationMethod: "custom", claims: someClaimsList); } else { context.Result = new GrantValidationResult( TokenRequestErrors.InvalidGrant, "invalid custom credential"); } return; }

Validación de la contraseña del propietario del recurso


En IdentityServer4. IUserService ya no está disponible, ahora debe usar IResourceOwnerPasswordValidator para realizar la autenticación y usar IProfileService para obtener las reclamaciones.

En mi caso, utilizo el tipo de concesión de propietario de recursos, y todo lo que necesito es obtener los reclamos de los usuarios para hacer autorizaciones basadas en roles para mis API web de acuerdo con el nombre de usuario y la contraseña. Y supuse que el tema es único para cada usuario.

He publicado mis códigos a continuación, y puede funcionar correctamente; ¿Alguien podría decirme que hay algún problema con mis códigos?

Registre estos dos servicios en startup.cs.

public void ConfigureServices(IServiceCollection services) { var builder = services.AddIdentityServer(); builder.AddInMemoryClients(Clients.Get()); builder.AddInMemoryScopes(Scopes.Get()); builder.Services.AddTransient<IResourceOwnerPasswordValidator, ResourceOwnerPasswordValidator>(); builder.Services.AddTransient<IProfileService, ProfileService>(); }

Implemente la interfaz IResourceOwnerPasswordValidator .

public class ResourceOwnerPasswordValidator: IResourceOwnerPasswordValidator { public Task<customgrantvalidationresult> ValidateAsync(string userName, string password, ValidatedTokenRequest request) { // Check The UserName And Password In Database, Return The Subject If Correct, Return Null Otherwise // subject = ...... if (subject == null) { var result = new CustomGrantValidationResult("Username Or Password Incorrect"); return Task.FromResult(result); } else { var result = new CustomGrantValidationResult(subject, "password"); return Task.FromResult(result); } } }

Implemente la interfaz ProfileService .

public class ProfileService : IProfileService { public Task GetProfileDataAsync(ProfileDataRequestContext context) { string subject = context.Subject.Claims.ToList().Find(s => s.Type == "sub").Value; try { // Get Claims From Database, And Use Subject To Find The Related Claims, As A Subject Is An Unique Identity Of User //List<string> claimStringList = ...... if (claimStringList == null) { return Task.FromResult(0); } else { List<Claim> claimList = new List<Claim>(); for (int i = 0; i < claimStringList.Count; i++) { claimList.Add(new Claim("role", claimStringList[i])); } context.IssuedClaims = claimList.Where(x => context.RequestedClaimTypes.Contains(x.Type)); return Task.FromResult(0); } } catch { return Task.FromResult(0); } } public Task IsActiveAsync(IsActiveContext context) { return Task.FromResult(0); } }


Actualización: IdentityServer 4 ha cambiado y reemplazado IUserService con IResourceOwnerPasswordValidator e IProfileService

Usé mi UserRepository para obtener todos los datos de usuario de la base de datos. Esto se inyecta (DI) en los constructores y se define en Startup.cs . También creé las siguientes clases para el servidor de identidad (que también se inyecta):

Primero defina ResourceOwnerPasswordValidator.cs :

public class ResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator { //repository to get user from db private readonly IUserRepository _userRepository; public ResourceOwnerPasswordValidator(IUserRepository userRepository) { _userRepository = userRepository; //DI } //this is used to validate your user account with provided grant at /connect/token public async Task ValidateAsync(ResourceOwnerPasswordValidationContext context) { try { //get your user model from db (by username - in my case its email) var user = await _userRepository.FindAsync(context.UserName); if (user != null) { //check if password match - remember to hash password if stored as hash in db if (user.Password == context.Password) { //set the result context.Result = new GrantValidationResult( subject: user.UserId.ToString(), authenticationMethod: "custom", claims: GetUserClaims(user)); return; } context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "Incorrect password"); return; } context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "User does not exist."); return; } catch (Exception ex) { context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "Invalid username or password"); } } //build claims array from user data public static Claim[] GetUserClaims(User user) { return new Claim[] { new Claim("user_id", user.UserId.ToString() ?? ""), new Claim(JwtClaimTypes.Name, (!string.IsNullOrEmpty(user.Firstname) && !string.IsNullOrEmpty(user.Lastname)) ? (user.Firstname + " " + user.Lastname) : ""), new Claim(JwtClaimTypes.GivenName, user.Firstname ?? ""), new Claim(JwtClaimTypes.FamilyName, user.Lastname ?? ""), new Claim(JwtClaimTypes.Email, user.Email ?? ""), new Claim("some_claim_you_want_to_see", user.Some_Data_From_User ?? ""), //roles new Claim(JwtClaimTypes.Role, user.Role) }; }

Y ProfileService.cs :

public class ProfileService : IProfileService { //services private readonly IUserRepository _userRepository; public ProfileService(IUserRepository userRepository) { _userRepository = userRepository; } //Get user profile date in terms of claims when calling /connect/userinfo public async Task GetProfileDataAsync(ProfileDataRequestContext context) { try { //depending on the scope accessing the user data. if (!string.IsNullOrEmpty(context.Subject.Identity.Name)) { //get user from db (in my case this is by email) var user = await _userRepository.FindAsync(context.Subject.Identity.Name); if (user != null) { var claims = GetUserClaims(user); //set issued claims to return context.IssuedClaims = claims.Where(x => context.RequestedClaimTypes.Contains(x.Type)).ToList(); } } else { //get subject from context (this was set ResourceOwnerPasswordValidator.ValidateAsync), //where and subject was set to my user id. var userId = context.Subject.Claims.FirstOrDefault(x => x.Type == "sub"); if (!string.IsNullOrEmpty(userId?.Value) && long.Parse(userId.Value) > 0) { //get user from db (find user by user id) var user = await _userRepository.FindAsync(long.Parse(userId.Value)); // issue the claims for the user if (user != null) { var claims = ResourceOwnerPasswordValidator.GetUserClaims(user); context.IssuedClaims = claims.Where(x => context.RequestedClaimTypes.Contains(x.Type)).ToList(); } } } } catch (Exception ex) { //log your error } } //check if user account is active. public async Task IsActiveAsync(IsActiveContext context) { try { //get subject from context (set in ResourceOwnerPasswordValidator.ValidateAsync), var userId = context.Subject.Claims.FirstOrDefault(x => x.Type == "user_id"); if (!string.IsNullOrEmpty(userId?.Value) && long.Parse(userId.Value) > 0) { var user = await _userRepository.FindAsync(long.Parse(userId.Value)); if (user != null) { if (user.IsActive) { context.IsActive = user.IsActive; } } } } catch (Exception ex) { //handle error logging } } }

Luego en Startup.cs hice lo siguiente:

public void ConfigureServices(IServiceCollection services) { //... //identity server 4 cert var cert = new X509Certificate2(Path.Combine(_environment.ContentRootPath, "idsrv4test.pfx"), "your_cert_password"); //DI DBContext inject connection string services.AddScoped(_ => new YourDbContext(Configuration.GetConnectionString("DefaultConnection"))); //my user repository services.AddScoped<IUserRepository, UserRepository>(); //add identity server 4 services.AddIdentityServer() .AddSigningCredential(cert) .AddInMemoryIdentityResources(Config.GetIdentityResources()) //check below .AddInMemoryApiResources(Config.GetApiResources()) .AddInMemoryClients(Config.GetClients()) .AddProfileService<ProfileService>(); //Inject the classes we just created services.AddTransient<IResourceOwnerPasswordValidator, ResourceOwnerPasswordValidator>(); services.AddTransient<IProfileService, ProfileService>(); //... } public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) { //... app.UseIdentityServer(); JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); IdentityServerAuthenticationOptions identityServerValidationOptions = new IdentityServerAuthenticationOptions { //move host url into appsettings.json Authority = "http://localhost:50000/", ApiSecret = "secret", ApiName = "my.api.resource", AutomaticAuthenticate = true, SupportedTokens = SupportedTokens.Both, // required if you want to return a 403 and not a 401 for forbidden responses AutomaticChallenge = true, //change this to true for SLL RequireHttpsMetadata = false }; app.UseIdentityServerAuthentication(identityServerValidationOptions); //... }

También necesitará Config.cs que define sus clientes, api y recursos. Puede encontrar un ejemplo aquí: https://github.com/IdentityServer/IdentityServer4.Demo/blob/master/src/IdentityServer4Demo/Config.cs

Ahora debería poder llamar a IdentityServer / connect / token

Para obtener más información, consulte la documentación: https://media.readthedocs.org/pdf/identityserver4/release/identityserver4.pdf

Respuesta anterior (esto ya no funciona para los nuevos IdentityServer4)

Es bastante simple una vez que entiendes el flujo de las cosas.

Configure su IdentityService de esta manera (en Startup.cs - ConfigureServices() ):

var builder = services.AddIdentityServer(options => { options.SigningCertificate = cert; }); builder.AddInMemoryClients(Clients.Get()); builder.AddInMemoryScopes(Scopes.Get()); //** this piece of code DI''s the UserService into IdentityServer ** builder.Services.AddTransient<IUserService, UserService>(); //for clarity of the next piece of code services.AddTransient<IUserRepository, UserRepository>();

Luego configure su servicio de usuario

public class UserService : IUserService { //DI the repository from Startup.cs - see previous code block private IUserRepository _userRepository; public UserService(IUserRepository userRepository) { _userRepository = userRepository; } public Task AuthenticateLocalAsync(LocalAuthenticationContext context) { var user = _userRepository.Find(context.UserName); //check if passwords match against user column //My password was hashed, //so I had to hash it with the saved salt first and then compare. if (user.Password == context.Password) { context.AuthenticateResult = new AuthenticateResult( user.UserId.ToString(), user.Email, //I set up some claims new Claim[] { //Firstname and Surname are DB columns mapped to User object (from table [User]) new Claim(Constants.ClaimTypes.Name, user.Firstname + " " + user.Surname), new Claim(Constants.ClaimTypes.Email, user.Email), new Claim(Constants.ClaimTypes.Role, user.Role.ToString()), //custom claim new Claim("company", user.Company) } ); } return Task.FromResult(0); } public Task GetProfileDataAsync(ProfileDataRequestContext context) { //find method in my repository to check my user email var user = _userRepository.Find(context.Subject.Identity.Name); if (user != null) { var claims = new Claim[] { new Claim(Constants.ClaimTypes.Name, user.Firstname + " " + user.Surname), new Claim(Constants.ClaimTypes.Email, user.Email), new Claim(Constants.ClaimTypes.Role, user.Role.ToString(), ClaimValueTypes.Integer), new Claim("company", user.Company) }; context.IssuedClaims = claims.Where(x => context.RequestedClaimTypes.Contains(x.Type)); } return Task.FromResult(0); } public Task IsActiveAsync(IsActiveContext context) { var user = _userRepository.Find(context.Subject.Identity.Name); return Task.FromResult(user != null); } }

Básicamente, al inyectar UserService en los servicios del builder (de tipo IdentityServerBuilder ), le permite llamar al UserService en autenticación.

Espero que esto ayude a otros, ya que me tomó un par de horas poner esto en marcha.