IdentityServer4 registra UserService y obtiene usuarios de la base de datos en el núcleo de asp.net

82

He buscado por todas partes cómo registrar un UserServicecon IdentityServer4 en el núcleo de asp.net, pero parece que no puedo encontrar la manera correcta de hacerlo.

Este es el código para registrar InMemoryUsers que se encuentra aquí , sin embargo, me gustaría acceder a los usuarios de mi base de datos MSSQL, no a los 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é esto 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 une al IdentityServer, por ejemplo.

services.AddScoped<IUserService, UserService>();

Entonces mi pregunta es:

¿Cómo enlazo mi UserServiceal constructor (usuarios de IdentityServer4)? ¿Y cómo haría para llamar 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!

Nick De Beer
fuente

Respuestas:

111

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

Usé mi UserRepository para obtener todos los datos del 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 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.cscuál 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

ingrese la descripción de la imagen aquí

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 IdentityServer4 más nuevo)

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

Configure su IdentityService así (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 UserService

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 UserServiceen builder(de tipo IdentityServerBuilder) Services, le permite llamar al UserService en auth.

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

Nick De Beer
fuente
10
Hmmm, por lo que puedo ver, IUserServiceen IdSvr4 (para ASP.NET Core 1.0) ya no existe. Ha sido reemplazado por dos interfaces / servicios IProfileServicey IResourceOwnerPasswordValidator.
Frank Fajardo
3
Sí, en el futuro, se dividirán. Preocupaciones separadas.
privilegio mínimo
3
@Sinaesthetic: lo siento mucho, identityserver4 se ha actualizado desde que se publicó esta respuesta y ya no usa IUserService. Actualicé mi respuesta, así que espero que esto ayude.
Nick De Beer
3
@Uros: debería poder llamar solo context.IssuedClaims = context.Subject.Claims.ToList();en GetProfileData, solo depende de si necesita ocultar algunas reclamaciones al público o si necesita hacer una lógica intermedia al ver los datos del perfil.
Nick De Beer
3
¿Es necesario actualizarlo para .net core 2? Implementé tanto IProfileServiece como IResourceOwnerPasswordValidator, pero el servidor de identidad no llama a ninguno de ellos.
stt106
66

En IdentityServer4. IUserServiceya no está disponible, ahora debe usar IResourceOwnerPasswordValidatorpara realizar la autenticación y usar IProfileServicepara obtener las reclamaciones.

En mi escenario, uso el tipo de concesión del propietario del recurso, y todo lo que necesito es obtener los reclamos de los usuarios para realizar una autorización basada en roles para mis API web de acuerdo con el nombre de usuario y la contraseña. Y asumí que el tema es único para cada usuario.

He publicado mis códigos a continuación y puede funcionar correctamente; ¿Alguien podría decirme si 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>();
}

Implementa la IResourceOwnerPasswordValidatorinterfaz.

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);
        }
    }
}

Implementa la ProfileServiceinterfaz.

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);
    }
}
EternidadWYH
fuente
He seguido esta respuesta, pero recibo el siguiente error: "Información adicional: No se ha especificado ningún mecanismo de almacenamiento para las subvenciones. Utilice el método de extensión 'AddInMemoryStores' para registrar una versión de desarrollo". Estoy usando "services.AddIdentityServer" para crear el constructor, la versión de IdentitiServer4 es 1.0.0-rc1-update2.
fra
Vale la pena señalar que si desea tomar el control de la reclamación "sub", debe realizar alguna personalización antes en la tubería.
Ben Collins
Es el mismo error que persiste, ¡incluso si proporciono implementación para ambos servicios!
Hussein Salman
@EternityWYH, ¿puedes echarle un vistazo a este [ stackoverflow.com/questions/40797993/…
Hussein Salman
Gracias por la respuesta, para mí fue suficiente implementar IResourceOwnerPasswordValidator e IProfileService en"IdentityServer4": "1.3.1"
Ilya Chumakov
9

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

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

 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

cruz
fuente