IdentityServer4는 UserService를 등록하고 asp.net 코어의 데이터베이스에서 사용자를 가져옵니다.


82

UserServiceasp.net 코어에서 IdentityServer4에 등록하는 방법을 모두 검색 했지만 올바른 방법을 찾지 못하는 것 같습니다.

이것은 여기 에있는 InMemoryUsers를 등록하는 코드 이지만 샘플에 정의 된 정적 사용자가 아닌 내 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 사용자)에 어떻게 바인드 합니까? 그리고 UserService(저는 리포지토리를 사용하여 db에 연결) 에서 기존 DB 사용자에 액세스하고 인증하기 위해 데이터베이스를 호출하는 방법은 무엇입니까?

고려하면이 작업을 asp.net 코어 .

감사!

답변:


112

업데이트 -IdentityServer 4가 IUserServiceIResourceOwnerPasswordValidatorIProfileService 로 변경하고 대체 했습니다.

UserRepository 를 사용 하여 데이터베이스에서 모든 사용자 데이터를 가져 왔습니다. 이것은 생성자에 주입 (DI)되고 Startup.cs. 또한 ID 서버에 대해 다음 클래스를 만들었습니다 (또한 삽입 됨).

먼저 정의 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
        }
    }
}

그런 Startup.cs다음 다음을 수행했습니다.

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

    //...
}

또한 Config.cs클라이언트, API 및 리소스를 정의하는 것이 필요 합니다. 여기에서 예제를 찾을 수 있습니다. https://github.com/IdentityServer/IdentityServer4.Demo/blob/master/src/IdentityServer4Demo/Config.cs

이제 IdentityServer / connect / token을 호출 할 수 있습니다.

여기에 이미지 설명 입력

추가 정보는 https://media.readthedocs.org/pdf/identityserver4/release/identityserver4.pdf 문서를 확인하십시오.


이전 답변 ( 더 이상 최신 IdentityServer4 에서는 작동 하지 않음 )

사물의 흐름을 이해하면 꽤 간단합니다.

다음과 같이 IdentityService를 구성합니다 (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>();

그런 다음 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);
    }
}

기본적 UserService으로 builder(유형의 IdentityServerBuilder) 에 주입 하여 Services인증시 UserService를 호출 할 수 있습니다.

이 작업을 수행하는 데 몇 시간이 걸리므로 다른 사람들에게 도움이되기를 바랍니다.


10
흠, 내가 볼 수 있듯이 IUserServiceIdSvr4 (ASP.NET Core 1.0 용)는 더 이상 존재하지 않습니다. 두 개의 인터페이스 / 서비스 IProfileServiceIResourceOwnerPasswordValidator.
Frank Fajardo

3
예-앞으로 나아갈 것입니다. 우려 사항을 분리하십시오.
leastprivilege

3
@Sinaesthetic-죄송합니다.이 답변이 게시 된 이후 identityserver4가 업데이트되었으며 더 이상 IUserService를 사용하지 않습니다. 내 답변을 업데이트 했으므로 도움이 되었기를 바랍니다.
Nick De Beer

3
@Uros- context.IssuedClaims = context.Subject.Claims.ToList();GetProfileData 에서만 호출 할 수 있어야합니다. 일부 클레임을 공개적으로 숨기거나 프로필 데이터를 볼 때 중개 논리를 수행해야하는 경우에만 다릅니다.
Nick De Beer

3
.net core 2에 대한 업데이트가 필요합니까? IProfileServiece와 IResourceOwnerPasswordValidator를 모두 구현했지만 ID 서버에서 호출되는 것은 없습니다.
stt106 dec.

66

IdentityServer4에서. IUserService더 이상 사용할 수 없습니다. 이제 IResourceOwnerPasswordValidator인증을 수행 IProfileService하고 클레임을 가져 오는 데을 사용해야 합니다.

내 시나리오에서는 리소스 소유자 권한 부여 유형을 사용하며 사용자 이름과 암호에 따라 내 웹 API에 대한 역할 기반 권한 부여를 수행하도록 사용자의 클레임을 얻는 것뿐입니다. 그리고 주제는 모든 사용자에게 고유하다고 가정했습니다.

아래에 내 코드를 게시했으며 제대로 작동 할 수 있습니다. 누구든지 내 코드에 문제가 있다고 말할 수 있습니까?

이 두 서비스를 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>();
}

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

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

이 대답을 따랐지만 "추가 정보 : 지정된 부여에 대한 저장 메커니즘이 없습니다. 'AddInMemoryStores'확장 메서드를 사용하여 개발 버전을 등록하십시오."라는 오류가 나타납니다. 빌더를 만들기 위해 "services.AddIdentityServer"를 사용하고 있습니다. IdentitiServer4의 버전은 1.0.0-rc1-update2입니다.
fra

"sub"클레임을 제어하려면 이보다 더 일찍 파이프 라인에서 몇 가지 사용자 지정을 수행해야한다는 점을 지적 할 가치가 있습니다.
Ben Collins

두 서비스 모두에 대한 구현을 제공하더라도 동일한 오류가 지속됩니다!
Hussein Salman 2016

@EternityWYH 당신이 [한 번 봐 걸릴 수 stackoverflow.com/questions/40797993/...을
후세인 살만

답변을 주셔서 감사합니다 나를 위해 그것을에서 IResourceOwnerPasswordValidator 및 IProfileService을 구현하기에 충분했다"IdentityServer4": "1.3.1"
일리아 Chumakov

9

IdentityServer4 1.0.0-rc5에서는 IUserService 나 CustomGrantValidationResult를 사용할 수 없습니다.

이제 CustomGrantValidationResult를 반환하는 대신 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;

   }

자원 소유자 비밀번호 검증

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.