일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- 쿼리 최적화
- TPL
- 영어공부
- execution plan
- fast in ssms
- SSMS
- 실행계획 원리
- async
- 느린 저장프로시저
- stored procedure
- MSSQL
- esl
- identityserver
- Dataannotation
- query
- task
- .net
- SQL Server Optimizer
- english
- IdentityServer4
- identityserver3
- await
- ThreadPool
- oauth2
- validation
- async await
- slow in the application
- C#
- SQLServer
- 저장프로시저
- Today
- Total
Genius DM
IdentityServer > Allow specific users in a client. 본문
아이덴티티 서버 클라이언트에서 특정 유저만 접근 허용하기.
스택오버플로우에 이런 질문이 몇 일전에 올라왔는데, 유저나 클라이언트 세팅만으로 간단하게 해결할 수 있을 줄 알았는데, 그런 직접적이고 간단한 방법이 없어서 생각보다 긴 답변을 작성하게 되었다. 요청이 진행 중일 때 이런 접근 제어를 하기위한 가장 적합한 방법을 찾아보았으나, 결국 사용자 정의 벨리데이션을 직접 구현하는 방법 밖에는 없었다.
ICustomRequestValidator
public class UserRequestLimitor : ICustomRequestValidator { public Task<AuthorizeRequestValidationResult> ValidateAuthorizeRequestAsync(ValidatedAuthorizeRequest request) { var clientClaim = request.Client.claims.Where(x => x.Type == "AllowedUsers").FirstOrDefault(); // Check is this client has "AllowedUsers" claim. if(clientClaim != null) { var subClaim = request.Subject.Claims.Where(x => x.Type == "sub").FirstOrDefault() ?? new Claim(string.Empty, string.Empty); if(clientClaim.Value == subClaim.Value) { return Task.FromResult<AuthorizeRequestValidationResult>(new AuthorizeRequestValidationResult { IsError = false }); } else { return Task.FromResult<AuthorizeRequestValidationResult>(new AuthorizeRequestValidationResult { ErrorDescription = "This user doesn't have an authorization to request a token for this client.", IsError = true }); } } // This client has no access controls over users. else { return Task.FromResult<AuthorizeRequestValidationResult>(new AuthorizeRequestValidationResult { IsError = false }); } } public Task<TokenRequestValidationResult> ValidateTokenRequestAsync(ValidatedTokenRequest request) { return Task.FromResult<TokenRequestValidationResult>(new TokenRequestValidationResult { IsError = false }); } }
이 방식은 다소 유연성이 부족해 보이는데, 열거형으로 정의되었다거나, 프로토콜 네이밍이 아닌 AllowedUsers 를 키로하여 Subject Id 를 값으로 구성한 Claim 을 넣었기 때문이다. 해당 아이디는 추후에 요청 과정에서 받아볼 수 있다. sub 키네임은 스탠다드이다. 아무튼 AllowedUsers 키 네이밍은 특정 유저의 엑세스를 제한하기 위해 만든 것이다.
제한 대상 유저 추가하기
유저 세팅은 여기서 진행한다. 아이덴티티 서버 기본 예제에서 볼 수 있는 전역 클래스 InMemoryUser 에서 아래처럼 유저를 추가할 수 있다.
static class Users { public static List<InMemoryUser> Get() { var users = new List<InMemoryUser> { new InMemoryUser{Subject = "818727", Username = "alice", Password = "alice", Claims = new Claim[] { new Claim(Constants.ClaimTypes.Name, "Alice Smith"), new Claim(Constants.ClaimTypes.GivenName, "Alice"), new Claim(Constants.ClaimTypes.FamilyName, "Smith"), new Claim(Constants.ClaimTypes.Email, "AliceSmith@email.com"), new Claim(Constants.ClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean), new Claim(Constants.ClaimTypes.Role, "Admin"), new Claim(Constants.ClaimTypes.Role, "Geek"), new Claim(Constants.ClaimTypes.WebSite, "http://alice.com"), new Claim(Constants.ClaimTypes.Address, @"{ ""street_address"": ""One Hacker Way"", ""locality"": ""Heidelberg"", ""postal_code"": 69118, ""country"": ""Germany"" }", Constants.ClaimValueTypes.Json) } }, new InMemoryUser{Subject = "88421113", Username = "bob", Password = "bob", Claims = new Claim[] { new Claim(Constants.ClaimTypes.Name, "Bob Smith"), new Claim(Constants.ClaimTypes.GivenName, "Bob"), new Claim(Constants.ClaimTypes.FamilyName, "Smith"), new Claim(Constants.ClaimTypes.Email, "BobSmith@email.com"), new Claim(Constants.ClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean), new Claim(Constants.ClaimTypes.Role, "Developer"), new Claim(Constants.ClaimTypes.Role, "Geek"), new Claim(Constants.ClaimTypes.WebSite, "http://bob.com"), new Claim(Constants.ClaimTypes.Address, @"{ ""street_address"": ""One Hacker Way"", ""locality"": ""Heidelberg"", ""postal_code"": 69118, ""country"": ""Germany"" }", Constants.ClaimValueTypes.Json) } },
// 제한 대상 유저 추가
new InMemoryUser{Subject = "870805", Username = "damon", Password = "damon", Claims = new Claim[] { new Claim(Constants.ClaimTypes.Name, "Damon Jeong"), new Claim(Constants.ClaimTypes.Email, "dmjeong@email.com"), new Claim(Constants.ClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean) } } };
return users; } }
대상 클라이언트에 클레임 추가하기.
public class Clients { public static List<Client> Get() { return new List<Client> { ///////////////////////////////////////////////////////////// // WPF WebView Client Sample ///////////////////////////////////////////////////////////// new Client { ClientName = "WPF WebView Client Sample", ClientId = "wpf.webview.client", Flow = Flows.Implicit, AllowedScopes = new List<string> { Constants.StandardScopes.OpenId, Constants.StandardScopes.Profile, Constants.StandardScopes.Email, Constants.StandardScopes.Roles, Constants.StandardScopes.Address, "read", "write" }, ClientUri = "https://identityserver.io", RequireConsent = true, AllowRememberConsent = true, RedirectUris = new List<string> { "oob://localhost/wpf.webview.client", },
// 클레임에 엑세스를 허용할 유저들을 추가한다.
Claims = new List<Claim>
{
new Claim("AllowedUsers", "870805"
} }, .
.
.
// Other clients. }; } }
구현한 클래스를 팩토리에 등록.
var factory = new IdentityServerServiceFactory(); factory.Register(new Registration<ICustomRequestValidator>(resolver => new UserRequestLimitor()));
동작 확인
Autofac IoC 에서 이미 구현 클래스를 인젝션했기 때문에 ValidateAuthorizeRequestAsync 가 수행되면 구현 코드로 디버거 브레이크 포인트가 넘어갈 것이다.
damon 으로 로그인을 하면, 디버거가 다시 한 번 구현 코드로 들어올 것이다. 그러나 이번에는 유저가 인증된 상태이기 때문에 ValidatedAuthorizeRequest 객체에 해당 유저 컨텍스트가 존재할 것이다. 해당 유저의 "sub" 값도 볼 수 있을 것이다.
유저 damon 은 벨리데이션을 당연히 통과한다. 왜냐하면 sub 아이디가 870805 이고, 해당 아이디가 클라이언트에 AllowedUsers 로 등록되어있기 때문이다. 만약 다른 유저로 로그인을 한다면, 아래와 같은 에러 페이지를 보게될 것이다.
Allow specific users in a client.
I saw this question in stackoverflow a few days ago and thought that this could be easily done with User or Client settings when configuring up an identity server. But there was no direct way to do access control on users in a specific client request context. I've been trying to find the best way to control users when the request is in flight but, unfortunately, to no avail. As far as I know so far, implementing a custom validation is the only way to do that.
ICustomRequestValidator
public class UserRequestLimitor : ICustomRequestValidator { public Task<AuthorizeRequestValidationResult> ValidateAuthorizeRequestAsync(ValidatedAuthorizeRequest request) { var clientClaim = request.Client.claims.Where(x => x.Type == "AllowedUsers").FirstOrDefault(); // Check is this client has "AllowedUsers" claim. if(clientClaim != null) { var subClaim = request.Subject.Claims.Where(x => x.Type == "sub").FirstOrDefault() ?? new Claim(string.Empty, string.Empty); if(clientClaim.Value == userClaim.Value) { return Task.FromResult<AuthorizeRequestValidationResult>(new AuthorizeRequestValidationResult { IsError = false }); } else { return Task.FromResult<AuthorizeRequestValidationResult>(new AuthorizeRequestValidationResult { ErrorDescription = "This user doesn't have an authorization to request a token for this client.", IsError = true }); } } // This client has no access controls over users. else { return Task.FromResult<AuthorizeRequestValidationResult>(new AuthorizeRequestValidationResult { IsError = false }); } } public Task<TokenRequestValidationResult> ValidateTokenRequestAsync(ValidatedTokenRequest request) { return Task.FromResult<TokenRequestValidationResult>(new TokenRequestValidationResult { IsError = false }); } }
This is rather a non-flexible way, it seems, because I added the "AllowedUsers" key name to the client object and the "sub" key name to retrieve the claim value. The "sub" key name is the default, so it doesn't matter anyway, but the "AllowedUsers" is what I've made to prevent the specified users.
Adding a target user.
The configuration is done in here. This example is in Users, the static class for InMemoryUser in a basic identity server example.
static class Users { public static List<InMemoryUser> Get() { var users = new List<InMemoryUser> { new InMemoryUser{Subject = "818727", Username = "alice", Password = "alice", Claims = new Claim[] { new Claim(Constants.ClaimTypes.Name, "Alice Smith"), new Claim(Constants.ClaimTypes.GivenName, "Alice"), new Claim(Constants.ClaimTypes.FamilyName, "Smith"), new Claim(Constants.ClaimTypes.Email, "AliceSmith@email.com"), new Claim(Constants.ClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean), new Claim(Constants.ClaimTypes.Role, "Admin"), new Claim(Constants.ClaimTypes.Role, "Geek"), new Claim(Constants.ClaimTypes.WebSite, "http://alice.com"), new Claim(Constants.ClaimTypes.Address, @"{ ""street_address"": ""One Hacker Way"", ""locality"": ""Heidelberg"", ""postal_code"": 69118, ""country"": ""Germany"" }", Constants.ClaimValueTypes.Json) } }, new InMemoryUser{Subject = "88421113", Username = "bob", Password = "bob", Claims = new Claim[] { new Claim(Constants.ClaimTypes.Name, "Bob Smith"), new Claim(Constants.ClaimTypes.GivenName, "Bob"), new Claim(Constants.ClaimTypes.FamilyName, "Smith"), new Claim(Constants.ClaimTypes.Email, "BobSmith@email.com"), new Claim(Constants.ClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean), new Claim(Constants.ClaimTypes.Role, "Developer"), new Claim(Constants.ClaimTypes.Role, "Geek"), new Claim(Constants.ClaimTypes.WebSite, "http://bob.com"), new Claim(Constants.ClaimTypes.Address, @"{ ""street_address"": ""One Hacker Way"", ""locality"": ""Heidelberg"", ""postal_code"": 69118, ""country"": ""Germany"" }", Constants.ClaimValueTypes.Json) } },
// Adding a target user here.
new InMemoryUser{Subject = "870805", Username = "damon", Password = "damon", Claims = new Claim[] { new Claim(Constants.ClaimTypes.Name, "Damon Jeong"), new Claim(Constants.ClaimTypes.Email, "dmjeong@email.com"), new Claim(Constants.ClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean) } } };
return users; } }
Adding a claim to a target client.
public class Clients { public static List<Client> Get() { return new List<Client> { ///////////////////////////////////////////////////////////// // WPF WebView Client Sample ///////////////////////////////////////////////////////////// new Client { ClientName = "WPF WebView Client Sample", ClientId = "wpf.webview.client", Flow = Flows.Implicit, AllowedScopes = new List<string> { Constants.StandardScopes.OpenId, Constants.StandardScopes.Profile, Constants.StandardScopes.Email, Constants.StandardScopes.Roles, Constants.StandardScopes.Address, "read", "write" }, ClientUri = "https://identityserver.io", RequireConsent = true, AllowRememberConsent = true, RedirectUris = new List<string> { "oob://localhost/wpf.webview.client", },
// Add users in this claim and its subject id.
Claims = new List<Claim>
{
new Claim("AllowedUsers", "870805"
} }, .
.
.
// Other clients. }; } }
Register your own implemented class to the factory.
var factory = new IdentityServerServiceFactory(); factory.Register(new Registration<ICustomRequestValidator>(resolver => new UserRequestLimitor()));
See if this works
As the Autofac IoC already injected our own implementation there, ValidateAuthorizeRequestAsync will be stepping into the implementation.
Again, after signing in with the user "damon", debugger will break at the implementation line again. But this time, the user is verified and the user context is included in the ValidatedAuthorizeRequest object. You can see "sub" value in the claims of the user.
The user damon will get through this validation because it has a "870805" subject id as the id is registered in the client's claim. If other users without "870805" id try to sign in, then the user will see this error page.
'.NET' 카테고리의 다른 글
C# > ThreadPool Basic (0) | 2018.08.20 |
---|---|
Image processing > Add a bitmap header to an image buffer. (0) | 2018.08.14 |
IIS > IIS Express & IIS applicationhost.config path (0) | 2018.08.06 |
IIS > IIS Express slow performance for initial requests. (0) | 2018.08.04 |
C# > Span<T> (0) | 2018.07.31 |