Search code examples
c#authenticationidentityserver3owin-middleware

Authenticating a particular user to a particular client using IdentityServer


I am using IdentityServerV3 for authenticating users for few clients. I am having problems configuring IdentityServer in such a way that a specific user must be able to login to a specific `client.

Let's take the below scenario :

I have 2 clients -> client1, client2. I have 3 users -> user1, user2, user3.

user1 & user2 have access to client1 only. Whereas user3 has access to client2 alone.

When i try to login to client1 with user3 identity server is authenticating successfully. Which i don't want.

public static class Clients
{
    public static IEnumerable<Client> Get()
    {
        return new[]
        {
            new Client 
            {
                Enabled = true,
                ClientName = "MVC Client",
                ClientId = "mvc",
                Flow = Flows.Hybrid,

                RedirectUris = new List<string>
                {
                    "https://localhost:44319/"
                }
            },
            new Client 
            {
                Enabled = true,
                ClientName = "MVC Client 2",
                ClientId = "mvc2",
                Flow = Flows.Hybrid,

                RedirectUris = new List<string>
                {
                    "https://localhost:44319/"
                }
            }
        };
    }
}

Users are :

public static class Users
{
    public static List<InMemoryUser> Get()
    {
        return new List<InMemoryUser>
        {
            new InMemoryUser
            {
                Username = "bob",
                Password = "secret",
                Subject = "1",

                Claims = new[]
                {
                    new Claim(Constants.ClaimTypes.GivenName, "Bob"),
                    new Claim(Constants.ClaimTypes.FamilyName, "Smith")
                }
            },
            new InMemoryUser
            {
                Username = "rob",
                Password = "secret",
                Subject = "2",

                Claims = new[]
                {
                    new Claim(Constants.ClaimTypes.GivenName, "Rob"),
                    new Claim(Constants.ClaimTypes.FamilyName, "Thompson")
                }
            },
            new InMemoryUser
            {
                Username = "Jerry",
                Password = "secret",
                Subject = "3",

                Claims = new[]
                {
                    new Claim(Constants.ClaimTypes.GivenName, "Jerry"),
                    new Claim(Constants.ClaimTypes.FamilyName, "Smith")
                }
            }
        };
    }
}

Now OWIN Startup class is :

public class Startup
{
    public void Configuration(IAppBuilder app)
    {
        app.Map("/identity", idsrvApp =>
            {
                idsrvApp.UseIdentityServer(new IdentityServerOptions
                {
                    SiteName = "Embedded IdentityServer",
                    SigningCertificate = LoadCertificate(),

                    Factory = InMemoryFactory.Create(
                        users  : Users.Get(),
                        clients: Clients.Get(),
                        scopes : StandardScopes.All)
                });
            });
    }

    X509Certificate2 LoadCertificate()
    {
        return new X509Certificate2(
            string.Format(@"{0}\bin\identityServer\idsrv3test.pfx", AppDomain.CurrentDomain.BaseDirectory), "idsrv3test");
    }
}

I don't see how IdentityServer knows which user bleongs to which client. Is my scenario not supported by IdentityServer or am i missing anything.

UPDATE

    public class LocalRegistrationUserService : UserServiceBase
    {
        public class CustomUser
        {
            public string Subject { get; set; }
            public string Username { get; set; }
            public string Password { get; set; }
            public List<Claim> Claims { get; set; }
        }

        public static List<CustomUser> Users = new List<CustomUser>();

        public string ClientId { get; set; }

        public override Task AuthenticateLocalAsync(LocalAuthenticationContext context)
        {
            var user = Users.SingleOrDefault(x => x.Username == context.UserName && x.Password == context.Password);
            if (user != null)
            {
                context.AuthenticateResult = new AuthenticateResult(user.Subject, user.Username);
            }

            return Task.FromResult(0);
        }
}

While registering the user i am redirecting him to a controller in Identity Server Host.

 public class LocalRegistrationController : Controller
    {
        [Route("core/localregistration")]
        [HttpGet]
        public ActionResult Index(string signin)
        {
            return View();
        }

        [Route("core/localregistration")]
        [HttpPost]
        public ActionResult Index(string signin, LocalRegistrationModel model)
        {
            var ctx = Request.GetOwinContext();
            if (ModelState.IsValid)
            {
                var user = new LocalRegistrationUserService.CustomUser
                {
                    Username = model.Username, 
                    Password = model.Password, 
                    Subject = Guid.NewGuid().ToString(),
                    Claims = new List<Claim>()
                };
                LocalRegistrationUserService.Users.Add(user);
                user.Claims.Add(new Claim(Constants.ClaimTypes.GivenName, model.First));
                user.Claims.Add(new Claim(Constants.ClaimTypes.FamilyName, model.Last));

                return Redirect("~/core/" + Constants.RoutePaths.Login + "?signin=" + signin);
            }

            return View();
        }
    }

startup.cs

 app.Map("/core", coreApp =>
            {
                var factory = new IdentityServerServiceFactory()
                    .UseInMemoryClients(Clients.Get())
                    .UseInMemoryScopes(Scopes.Get());

                // different examples of custom user services
                //var userService = new RegisterFirstExternalRegistrationUserService();
                //var userService = new ExternalRegistrationUserService();

                var userService = new LocalRegistrationUserService();

                // note: for the sample this registration is a singletone (not what you want in production probably)
                factory.UserService = new Registration<IUserService>(resolver => userService);
                factory.ViewService = new Registration<IViewService, CustomViewService>();
                var options = new IdentityServerOptions
                {
                    SiteName = "Identity Server 3",

                    SigningCertificate = Certificate.Get(),
                    Factory = factory,

                    AuthenticationOptions = new AuthenticationOptions
                    {
                        IdentityProviders = ConfigureAdditionalIdentityProviders,
                        LoginPageLinks = new LoginPageLink[] { 
                            new LoginPageLink{
                                Text = "Register",
                               // Href = "~/externalregistration",
                                Href = "~/localregistration",
                                //Href = "localregistration"
                            },
                             new LoginPageLink{
                                Text = "Forgot Password?",
                                Href = "~/forgotpassword",
                                //Href = "localregistration"
                            }
                        }
                    },

                    EventsOptions = new EventsOptions
                    {
                        RaiseSuccessEvents = true,
                        RaiseErrorEvents = true,
                        RaiseFailureEvents = true,
                        RaiseInformationEvents = true
                    }
                };

                coreApp.UseIdentityServer(options);
            });

I need to understand the best way to map a user to client while registering and make sure that user can only login to that client and not any other clients even though the username and password are same for all the clients for that user.


Solution

  • What if you provide a custom claim to go in your id_token, named application_access with a value of an application/client the user has access to; which of there could be many. You could add this custom claim in your IUserService implementation and map this the same way you map other user specific claims.

    Then, in your client, do a check for that specific claim and check if the logged in user has a claim with a specific value for that specific client.

    https://github.com/IdentityModel/Thinktecture.IdentityModel.45/blob/master/IdentityModel/Thinktecture.IdentityModel/Authorization/WebApi/ScopeAttribute.cs

    (mark: RequireScope is for access tokens and APIs, but you get the general idea)