IdentityServer4 registriert UserService und ruft Benutzer aus der Datenbank im asp.net-Kern ab

83

Ich habe überall gesucht, wie man einen UserServicebei IdentityServer4 im asp.net-Kern registriert , aber ich kann anscheinend nicht den richtigen Weg finden, dies zu tun.

Dies ist der Code zum Registrieren von InMemoryUsers, der hier zu finden ist. Ich möchte jedoch auf Benutzer aus meiner MSSQL- Datenbank zugreifen, die nicht statische Benutzer sind, die im Beispiel definiert sind.

var builder = services.AddIdentityServer(options =>
{
    options.SigningCertificate = cert;
});

builder.AddInMemoryClients(Clients.Get());
builder.AddInMemoryScopes(Scopes.Get());
builder.AddInMemoryUsers(Users.Get());

Also schaute ich auf diese , die für ist IdentityServer3 .

var factory = new IdentityServerServiceFactory()
                .UseInMemoryClients(Clients.Get())
                .UseInMemoryScopes(Scopes.Get());

var userService = new UserService();
factory.UserService = new Registration<IUserService>(resolver => userService);

Nach dem Online-Lesen muss ich das DI-System verwenden, um den UserService zu registrieren, bin mir jedoch nicht sicher, wie er an den IdentityServer gebunden wird, z.

services.AddScoped<IUserService, UserService>();

Meine Frage lautet also:

Wie binde ich meine UserServicean den Builder (IdentityServer4-Benutzer)? Und wie würde ich meine Datenbank aufrufen, um auf meine vorhandenen Datenbankbenutzer in der Datenbank zuzugreifen und diese zu authentifizieren UserService(ich verwende Repositorys, um eine Verbindung zu der Datenbank herzustellen)?

Unter Berücksichtigung dieser hat die Arbeit mit asp.net Kern .

Vielen Dank!

Nick De Beer
quelle

Antworten:

115

Update - IdentityServer 4 hat sich verändert und ersetzt IUserService mit IResourceOwnerPasswordValidator und IProfileService

Ich habe mein UserRepository verwendet , um alle Benutzerdaten aus der Datenbank abzurufen . Dies wird in die Konstruktoren injiziert (DI) und in definiert Startup.cs. Ich habe auch die folgenden Klassen für den Identitätsserver erstellt (der ebenfalls injiziert wird):

Definieren Sie zuerst 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)
        };
}

Und 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
        }
    }
}

Dann habe Startup.csich folgendes gemacht:

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

    //...
}

Sie benötigen auch Config.csdie Definition Ihrer Kunden, APIs und Ressourcen. Ein Beispiel finden Sie hier: https://github.com/IdentityServer/IdentityServer4.Demo/blob/master/src/IdentityServer4Demo/Config.cs

Sie sollten jetzt IdentityServer / connect / token aufrufen können

Geben Sie hier die Bildbeschreibung ein

Weitere Informationen finden Sie in der Dokumentation: https://media.readthedocs.org/pdf/identityserver4/release/identityserver4.pdf


Alte Antwort (dies funktioniert nicht mehr für neuere IdentityServer4)

Es ist ziemlich einfach, wenn Sie den Fluss der Dinge verstanden haben.

Konfigurieren Sie Ihren IdentityService folgendermaßen (in 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>();

Richten Sie dann Ihren UserService ein

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

Grundsätzlich ermöglicht es durch Injizieren UserServicein builder(vom Typ IdentityServerBuilder) Services, den UserService bei auth aufzurufen.

Ich hoffe, das hilft anderen, da ich ein paar Stunden gebraucht habe, um das in Gang zu bringen.

Nick De Beer
quelle
10
Hmmm, soweit ich sehen kann, existiert das IUserServiceauf IdSvr4 (für ASP.NET Core 1.0) nicht mehr. Es wurde durch zwei Schnittstellen / Dienste IProfileServiceund ersetzt IResourceOwnerPasswordValidator.
Frank Fajardo
3
Ja - in Zukunft werden sie aufgeteilt. Getrennte Bedenken.
wenigsten Privileg
3
@Sinaesthetic - Entschuldigung, Identityserver4 wurde aktualisiert, seit diese Antwort veröffentlicht wurde, und verwendet IUserService nicht mehr. Ich habe meine Antwort aktualisiert und hoffe, dass dies hilft.
Nick De Beer
3
@Uros - Sie sollten in der Lage sein, nur context.IssuedClaims = context.Subject.Claims.ToList();GetProfileData aufzurufen. Dies hängt nur davon ab, ob Sie einige Ansprüche vor der Öffentlichkeit verbergen oder beim Anzeigen von Profildaten eine Zwischenlogik ausführen müssen.
Nick De Beer
3
Muss dies für .net Core 2 aktualisiert werden? Ich habe sowohl IProfileServiece als auch IResourceOwnerPasswordValidator implementiert, aber keiner von ihnen wird vom Identitätsserver aufgerufen.
stt106
66

In IdentityServer4. IUserServiceist nicht mehr verfügbar, jetzt müssen Sie verwenden IResourceOwnerPasswordValidator, um die Authentifizierung durchzuführen und IProfileServiceum die Ansprüche zu erhalten.

In meinem Szenario verwende ich den Grant-Typ des Ressourcenbesitzers. Ich muss lediglich die Ansprüche der Benutzer dazu bringen, eine rollenbasierte Autorisierung für meine Web-APIs gemäß Benutzername und Kennwort durchzuführen. Und ich nahm an, dass das Thema für jeden Benutzer einzigartig ist.

Ich habe meine Codes unten veröffentlicht und es kann richtig funktionieren. Kann mir jemand sagen, dass es Probleme mit meinen Codes gibt?

Registrieren Sie diese beiden Dienste in der Datei 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>();
}

Implementieren Sie die IResourceOwnerPasswordValidatorSchnittstelle.

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

Implementieren Sie die ProfileServiceSchnittstelle.

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);
    }
}
EwigkeitWYH
quelle
Ich habe diese Antwort befolgt, erhalte jedoch die folgende Fehlermeldung: "Zusätzliche Informationen: Kein Speichermechanismus für Berechtigungen angegeben. Verwenden Sie die Erweiterungsmethode 'AddInMemoryStores', um eine Entwicklungsversion zu registrieren." Ich verwende "services.AddIdentityServer", um den Builder zu erstellen. Die Version von IdentitiServer4 ist 1.0.0-rc1-update2.
fra
Es ist erwähnenswert, dass Sie, wenn Sie die Kontrolle über den "Sub" -Anspruch übernehmen möchten, einige Anpassungen früher in der Pipeline vornehmen müssen.
Ben Collins
Es ist der gleiche Fehler, der weiterhin besteht, auch wenn ich die Implementierung für beide Dienste bereitstelle!
Hussein Salman
@ EwigkeitWYH können Sie einen Blick auf diese [ stackoverflow.com/questions/40797993/…
Hussein Salman
Vielen Dank für die Antwort, für mich war es genug, IResourceOwnerPasswordValidator und IProfileService in"IdentityServer4": "1.3.1"
Ilya
10

In IdentityServer4 1.0.0-rc5 ist weder IUserService noch CustomGrantValidationResult verfügbar.

Anstatt ein CustomGrantValidationResult zurückzugeben, müssen Sie jetzt den context.Result festlegen.

 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;

   }

Überprüfung des Kennworts des Ressourcenbesitzers

rood
quelle