IdentityServer4 đăng ký UserService và lấy người dùng từ cơ sở dữ liệu trong lõi asp.net


82

Tôi đã tìm kiếm khắp nơi về cách đăng ký UserServicevới IdentityServer4 trong lõi asp.net, nhưng dường như tôi không thể tìm ra cách thích hợp để làm điều đó.

Đây là mã để đăng ký InMemoryUsers được tìm thấy ở đây , tuy nhiên tôi muốn truy cập người dùng từ MSSQL DB của mình chứ không phải người dùng tĩnh được xác định trong mẫu.

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

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

Vì vậy, sau đó tôi đã xem xét cái này dành cho IdentityServer3 .

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

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

Từ việc đọc trực tuyến, có vẻ như tôi cần sử dụng hệ thống DI để đăng ký UserService, nhưng tôi không chắc nó liên kết với IdentityServer như thế nào, ví dụ.

services.AddScoped<IUserService, UserService>();

Vì vậy, câu hỏi của tôi là:

Làm cách nào để ràng buộc tôi UserServicevới trình tạo (Người dùng IdentityServer4)? Và tôi sẽ làm cách nào để gọi cơ sở dữ liệu của mình để truy cập và xác thực người dùng db hiện có của tôi trong UserService(Tôi sử dụng kho để kết nối với db)?

Có tính đến điều này phải làm việc với lõi asp.net .

Cảm ơn!

Câu trả lời:


111

Cập nhật - IdentityServer 4 đã thay đổi và thay thế IUserService bằng IResourceOwnerPasswordValidatorIProfileService

Tôi đã sử dụng UserRepository của mình để lấy tất cả dữ liệu người dùng từ cơ sở dữ liệu. Điều này được tiêm (DI) vào các hàm tạo và được định nghĩa trong Startup.cs. Tôi cũng đã tạo các lớp sau cho máy chủ nhận dạng (cũng được đưa vào):

Đầu tiên xác định 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)
        };
}

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

Sau đó, Startup.cstôi đã làm như sau:

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

    //...
}

Bạn cũng sẽ cần Config.csđịnh nghĩa khách hàng, api và tài nguyên của bạn. Bạn có thể tìm một ví dụ ở đây: https://github.com/IdentityServer/IdentityServer4.Demo/blob/master/src/IdentityServer4Demo/Config.cs

Bây giờ bạn có thể gọi IdentityServer / connect / token

nhập mô tả hình ảnh ở đây

Để biết thêm thông tin, vui lòng kiểm tra tài liệu: https://media.readthedocs.org/pdf/identityserver4/release/identityserver4.pdf


Câu trả lời cũ (điều này không hoạt động cho IdentityServer4 mới hơn nữa)

Nó khá đơn giản khi bạn hiểu được dòng chảy của mọi thứ.

Định cấu hình IdentityService của bạn như thế này (trong 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>();

Sau đó, thiết lập UserService của bạn

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

Về cơ bản bằng cách tiêm UserServicevào builder(loại IdentityServerBuilder) Services, cho phép nó gọi UserService trên auth.

Tôi hy vọng điều này sẽ giúp ích cho những người khác vì tôi đã mất vài giờ để thực hiện điều này.


10
Hmmm, từ những gì tôi có thể thấy, IUserServicetrên IdSvr4 (cho ASP.NET Core 1.0) không còn tồn tại nữa. Nó đã được thay thế bằng hai giao diện / dịch vụ IProfileServiceIResourceOwnerPasswordValidator.
Frank Fajardo

3
Có - tiếp tục - chúng sẽ bị chia rẽ. Mối quan tâm riêng biệt.
ít đặc quyền nhất

3
@Sinaesthetic - Rất tiếc về điều đó, IDserver4 đã được cập nhật kể từ khi câu trả lời này được đăng và không sử dụng IUserService nữa. Tôi đã cập nhật câu trả lời của mình, vì vậy tôi hy vọng điều này sẽ hữu ích.
Nick De Beer

3
@Uros - Bạn chỉ có thể gọi context.IssuedClaims = context.Subject.Claims.ToList();trong GetProfileData, chỉ tùy thuộc vào việc bạn cần ẩn một số xác nhận quyền sở hữu với công chúng hoặc cần thực hiện một số logic trung gian khi xem dữ liệu hồ sơ.
Nick De Beer,

3
Điều này có cần cập nhật cho .net core 2 không? Tôi đã triển khai cả IProfileServiece và IResourceOwnerPasswordValidator nhưng không ai trong số chúng được máy chủ nhận dạng gọi.
stt106 27/12/17

66

Trong IdentityServer4. IUserServicekhông khả dụng nữa, bây giờ bạn phải sử dụng IResourceOwnerPasswordValidatorđể xác thực và sử dụng IProfileServiceđể nhận các xác nhận quyền sở hữu.

Trong trường hợp của tôi, tôi sử dụng kiểu cấp của chủ sở hữu tài nguyên và tất cả những gì tôi cần là yêu cầu người dùng xác nhận quyền dựa trên vai trò cho các API Web của tôi theo tên người dùng và mật khẩu. Và tôi đã giả định rằng chủ đề là duy nhất cho mọi người dùng.

Tôi đã đăng mã của mình bên dưới và nó có thể hoạt động bình thường; bất cứ ai có thể cho tôi biết rằng có bất kỳ vấn đề nào về mã của tôi không?

Đăng ký hai dịch vụ này trong 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>();
}

Triển khai IResourceOwnerPasswordValidatorgiao diện.

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

Triển khai ProfileServicegiao diện.

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

Tôi đã làm theo câu trả lời này, nhưng tôi nhận được lỗi sau: "Thông tin bổ sung: Không có cơ chế lưu trữ cho các khoản tài trợ được chỉ định. Sử dụng phương thức mở rộng 'AddInMemoryStores' để đăng ký phiên bản phát triển". Tôi đang sử dụng "services.AddIdentityServer" để tạo trình tạo, phiên bản của IdentitiServer4 là 1.0.0-rc1-update2.
fra

Cần chỉ ra rằng nếu bạn muốn kiểm soát xác nhận quyền sở hữu "phụ", bạn phải thực hiện một số tùy chỉnh sớm hơn trong quá trình này.
Ben Collins

Lỗi tương tự vẫn tiếp diễn, ngay cả khi tôi cung cấp triển khai cho cả hai dịch vụ!
Hussein Salman

@EternityWYH bạn có thể xem điều này không [ stackoverflow.com/questions/40797993/…
Hussein Salman,

Cảm ơn cho câu trả lời, đối với tôi nó là đủ để thực hiện IResourceOwnerPasswordValidator và IProfileService trong"IdentityServer4": "1.3.1"
Ilya Chumakov

9

Trong IdentityServer4 1.0.0-rc5 không có IUserService và CustomGrantValidationResult.

Bây giờ thay vì trả về CustomGrantValidationResult, bạn sẽ cần đặt bối cảnh.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;

   }

Xác thực mật khẩu của chủ sở hữu tài nguyên

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.