IdentityServer4 UserService को पंजीकृत करें और asp.net कोर में डेटाबेस से उपयोगकर्ताओं को प्राप्त करें


84

मैं UserServiceasp.net कोर में IdentityServer4 के साथ पंजीकरण करने के तरीके के बारे में सभी खोज कर चुका हूं , लेकिन मुझे ऐसा करने का सही तरीका नहीं लगता है।

यह यहाँ पाए जाने वाले इनमोरोरीज को पंजीकृत करने का कोड है , हालाँकि मैं अपने MSSQL DB के उपयोगकर्ताओं को नमूने में परिभाषित स्थिर उपयोगकर्ताओं तक नहीं पहुँचना चाहूँगा।

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

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

तो फिर मैंने इस पर ध्यान दिया जो कि IdentityServer3 के लिए है

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

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

ऑनलाइन पढ़ने से ऐसा लगता है कि मुझे UserService को पंजीकृत करने के लिए DI सिस्टम का उपयोग करने की आवश्यकता है, लेकिन मुझे यकीन नहीं है कि यह IdentityServer जैसे कैसे बांधता है।

services.AddScoped<IUserService, UserService>();

तो मेरा सवाल है:

मैं UserServiceबिल्डर (IdentityServer4 उपयोगकर्ता) से कैसे बंध सकता हूं ? और मैं अपने मौजूदा db उपयोगकर्ताओं को एक्सेस करने और प्रमाणित करने के लिए अपने डेटाबेस को कॉल करने के बारे में कैसे जाऊंगा UserService(मैं db से कनेक्ट करने के लिए रिपॉजिटरी का उपयोग करता हूं)?

ध्यान में रखते हुए इस है के साथ काम करने asp.net कोर

धन्यवाद!

जवाबों:


116

अपडेट - आइडेंटिटी सेवर 4 को बदल दिया गया है और इसे IUserService को IResourceOwnerPasswordValidator और IProfileService से बदल दिया गया है

मैंने डेटाबेस से सभी उपयोगकर्ता डेटा प्राप्त करने के लिए अपने UserRepository का उपयोग किया । यह कंस्ट्रक्टर्स में इंजेक्ट (DI) है, और इसमें परिभाषित किया गया है Startup.cs। मैंने पहचान सर्वर के लिए निम्न वर्ग भी बनाए हैं (जो इंजेक्शन भी है):

पहले परिभाषित करें 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)
        };
}

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

Then in Startup.cs I did the following:

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

    //...
}

You will also need Config.cs which defines your clients, api's and resources. You can find an example here: https://github.com/IdentityServer/IdentityServer4.Demo/blob/master/src/IdentityServer4Demo/Config.cs

You should now be able to call IdentityServer /connect/token

enter image description here

For any further info, please check the documentation: https://media.readthedocs.org/pdf/identityserver4/release/identityserver4.pdf


Old answer (this does not work for newer IdentityServer4 anymore)

Its pretty simple once you understand the flow of things.

Configure your IdentityService like this (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>();

Then setup your 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);
    }
}

Basically by injecting UserService into builder (of type IdentityServerBuilder) Services, allows it to call the UserService on auth.

I hope this helps others as it took me a couple of hours to get this going.


10
Hmmm, from what I can see, the IUserService on IdSvr4 (for ASP.NET Core 1.0) no longer exists. It has been replaced by two interfaces/services IProfileService and IResourceOwnerPasswordValidator.
Frank Fajardo

3
Yes - going forward - they will be split. Separate concerns.
leastprivilege

3
@Sinaesthetic - So sorry about that, identityserver4 has been updated since this answer was posted and does not use IUserService anymore. I've updated my answer, so I hope this helps.
Nick De Beer

3
@Uros - You should be able to call just context.IssuedClaims = context.Subject.Claims.ToList(); in GetProfileData, just depends if you need to hide some claims from the public or need to do some intermediary logic when viewing profile data.
Nick De Beer

3
Does this need updating for .net core 2? I implemented both IProfileServiece and IResourceOwnerPasswordValidator but none of them gets called by the identity server.
stt106

66

In IdentityServer4. IUserService is not available anymore, now you have to use IResourceOwnerPasswordValidator to do the authentication and to use IProfileService to get the claims.

In my scenario, I use resource owner grant type, and all I need is to get users' claims to do role based authorization for my Web APIs according to the username and password. And I assumed that the subject is unique for every user.

I have posted my codes below, and it can work properly; could anyone tell me that is there any issues about my codes?

Register these two services in the 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>();
}

Implement the IResourceOwnerPasswordValidator interface.

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

Implement the ProfileService interface.

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

I've followed this answer, but I receive the following error: "Additional information: No storage mechanism for grants specified. Use the 'AddInMemoryStores' extension method to register a development version". I'm using "services.AddIdentityServer" to create the builder, the version of IdentitiServer4 is 1.0.0-rc1-update2.
fra

It's worth pointing out that if you want to take control of the "sub" claim, you have to do some customization earlier in the pipeline than this.
Ben Collins

Its the same error persisting, even if I provide implementation for both services!
Hussein Salman

@EternityWYH can you take a look at this [stackoverflow.com/questions/40797993/…
Hussein Salman

Thanks for answer, for me it was enough to implement IResourceOwnerPasswordValidator and IProfileService in "IdentityServer4": "1.3.1"
Ilya Chumakov

10

In IdentityServer4 1.0.0-rc5 neither IUserService nor CustomGrantValidationResult is available.

Now instead of returning an CustomGrantValidationResult you will need to set the 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;

   }

Resource Owner Password Validation

हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.