
IdentityServer peut �tre utilis� par les entreprises pour mettre en place une solution pour :
- la protection de leurs ressources ;
- l�authentification des utilisateurs via une base de donn�es ou des fournisseurs externes d�identit� (Microsoft, Google, Facebook, etc.) ;
- la gestion des sessions et la f�d�ration (single sign-on) ;
- la g�n�ration des jetons pour les clients ;
- la validation des jetons et bien plus.
Ce billet est le onzi�me que j��cris sur le sujet. Les billets pr�c�dents ont port� sur les points suivants :










Dans l'un des billets pr�c�dents, nous avons vu comment utiliser OpenID et permettre � l�utilisateur de s�authentifier via un formulaire. Pour la mise en place de la fen�tre de connexion, de d�connexion, etc., nous avons utilis� un Quickstart offert par IdentityServer. Ce mod�le repose sur TestUserStore, qui nous permet de d�finir et charger nos utilisateurs depuis un fichier inclus dans le projet.
Le TestUserStore est offert � des fins de tests pour permettre aux d�veloppeurs de d�marrer facilement avec la prise en main de l�outil. Dans un projet concret d�entreprise, vous aurez votre propre base de donn�es utilisateurs et utiliserez ce dernier pour l�authentification.
Dans ce billet, nous verrons comment authentifier l�utilisateur en utilisant notre propre service d�acc�s aux donn�es et comment d�finir les revendications de l�utilisateur.
Nous utiliserons comme projet de base la solution suivante qui est disponible sur mon GitHub : https://github.com/hinault/identitys...ee/aspnetcore2.
Cr�ation du service
La premi�re chose � faire sera de d�finir notre classe entit� Utilisateur. Pour cela, nous allons cr�er dans le dossier Model la classe CustomUser suivante :
Code c# : | S�lectionner tout |
1 2 3 4 5 6 7 8 | public class CustomUser { public string SubjectId { get; set; } public string UserName { get; set; } public string Password { get; set; } public string FirstName { get; set; } public string Email { get; set; } } |
La deuxi�me �tape sera la cr�ation du UserRepository. Nous allons tout d�abord cr�er l�interface IUserRepository dans le dossier Repository du projet :
Code c# : | S�lectionner tout |
1 2 3 4 5 6 7 8 9 | public interface IUserRepository { CustomUser FindByUserName(string userName); CustomUser FindBySubjectId(string subjectId); bool ValidateCredentials(string userName, string password); } |
Ensuite, nous allons ajouter l�impl�mentation de cette interface :
Code c# : | S�lectionner tout |
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 31 32 33 34 35 36 37 38 | public class UserRepository : IUserRepository { private List<CustomUser> _customUsers = new List<CustomUser> { new CustomUser { SubjectId = "1111", UserName = "alice", FirstName = "Alice Smith", Email = "AliceSmith@email.com", Password = "alice" }, new CustomUser { SubjectId = "2222", UserName = "bob", FirstName = "Bob Smith", Email = "BobSmith@email.com", Password = "bob" } }; public CustomUser FindBySubjectId(string subjectId) { return _customUsers.Find(x => x.SubjectId.Equals(subjectId)); } public CustomUser FindByUserName(string userName) { return _customUsers.Find(x=>x.UserName.Equals(userName)); } public bool ValidateCredentials(string userName, string password) { var customUser = FindByUserName(userName); return customUser != null && customUser.Password.Equals(password); } } |
Afin que l�initialisation de ce service puisse se faire correctement, nous devons enregistrer celui-ci dans notre conteneur d�IoC ASP.NET Core. Vous devez �diter la m�thode ConfigureServices du fichier Startup.cs et ajouter la ligne de code suivante :
Code c# : | S�lectionner tout |
services.AddTransient<IUserRepository, UserRepository>();
Pour en savoir plus sur l�injection de d�pendances avec ASP.NET Core, veuillez consulter mon billet de blog suivant : https://www.developpez.net/forums/bl...-asp-net-core/
Le contr�leur AccourntController doit �tre modifi� pour utiliser le UserRepository et sa m�thode ValidateCredentials() pour valider l�identit� de l�utilisateur.
Code c# : | S�lectionner tout |
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 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 | private readonly IUserRepository _userRepository; public AccountController( IIdentityServerInteractionService interaction, IClientStore clientStore, IAuthenticationSchemeProvider schemeProvider, IEventService events, IUserRepository userRepository) { _interaction = interaction; _clientStore = clientStore; _schemeProvider = schemeProvider; _events = events; _userRepository = userRepository; } /// <summary> /// Entry point into the login workflow /// </summary> [HttpGet] public async Task<IActionResult> Login(string returnUrl) { // build a model so we know what to show on the login page var vm = await BuildLoginViewModelAsync(returnUrl); if (vm.IsExternalLoginOnly) { // we only have one option for logging in and it's an external provider return RedirectToAction("Challenge", "External", new { provider = vm.ExternalLoginScheme, returnUrl }); } return View(vm); } /// <summary> /// Handle postback from username/password login /// </summary> [HttpPost] [ValidateAntiForgeryToken] public async Task<IActionResult> Login(LoginInputModel model, string button) { // check if we are in the context of an authorization request var context = await _interaction.GetAuthorizationContextAsync(model.ReturnUrl); // the user clicked the "cancel" button if (button != "login") { if (context != null) { // if the user cancels, send a result back into IdentityServer as if they // denied the consent (even if this client does not require consent). // this will send back an access denied OIDC error response to the client. await _interaction.GrantConsentAsync(context, ConsentResponse.Denied); // we can trust model.ReturnUrl since GetAuthorizationContextAsync returned non-null if (await _clientStore.IsPkceClientAsync(context.ClientId)) { // if the client is PKCE then we assume it's native, so this change in how to // return the response is for better UX for the end user. return View("Redirect", new RedirectViewModel { RedirectUrl = model.ReturnUrl }); } return Redirect(model.ReturnUrl); } else { // since we don't have a valid context, then we just go back to the home page return Redirect("~/"); } } if (ModelState.IsValid) { // validate username/password against in-memory store if (_userRepository.ValidateCredentials(model.Username, model.Password)) { var user = _userRepository.FindByUserName(model.Username); await _events.RaiseAsync(new UserLoginSuccessEvent(user.UserName, user.SubjectId, user.UserName)); // only set explicit expiration here if user chooses "remember me". // otherwise we rely upon expiration configured in cookie middleware. AuthenticationProperties props = null; if (AccountOptions.AllowRememberLogin && model.RememberLogin) { props = new AuthenticationProperties { IsPersistent = true, ExpiresUtc = DateTimeOffset.UtcNow.Add(AccountOptions.RememberMeLoginDuration) }; }; // issue authentication cookie with subject ID and username await HttpContext.SignInAsync(user.SubjectId, user.UserName, props); if (context != null) { if (await _clientStore.IsPkceClientAsync(context.ClientId)) { // if the client is PKCE then we assume it's native, so this change in how to // return the response is for better UX for the end user. return View("Redirect", new RedirectViewModel { RedirectUrl = model.ReturnUrl }); } // we can trust model.ReturnUrl since GetAuthorizationContextAsync returned non-null return Redirect(model.ReturnUrl); } // request for a local page if (Url.IsLocalUrl(model.ReturnUrl)) { return Redirect(model.ReturnUrl); } else if (string.IsNullOrEmpty(model.ReturnUrl)) { return Redirect("~/"); } else { // user might have clicked on a malicious link - should be logged throw new Exception("invalid return URL"); } } await _events.RaiseAsync(new UserLoginFailureEvent(model.Username, "invalid credentials")); ModelState.AddModelError("", AccountOptions.InvalidCredentialsErrorMessage); } |
Impl�mentation de l�interface IProfileService
Nous voulons que certaines informations de l�utilisateur (Email, nom, etc.) soient partag�es avec les applications qui viennent s�authentifier via IdentityServer. Ces informations doivent �tre incluses dans les Claims (Revendications). Pour mettre cela en place, nous devons fournir notre propre impl�mentation de l�interface IProfileService :
Code c# : | S�lectionner tout |
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 | public class ProfileService : IProfileService { private IUserRepository _userRepository { get; set; } public ProfileService(IUserRepository userRepository) { _userRepository = userRepository; } public Task GetProfileDataAsync(ProfileDataRequestContext context) { var custormUser = _userRepository.FindBySubjectId(context.Subject.FindFirst(x => x.Type == "sub").Value); if (custormUser != null) { context.IssuedClaims = GetClaims(custormUser); } return Task.FromResult(0); } public Task IsActiveAsync(IsActiveContext context) { return Task.FromResult(0); } private List<Claim> GetClaims(CustomUser customUser) => new List<Claim> { new Claim(JwtClaimTypes.Name, customUser.UserName), new Claim(JwtClaimTypes.FamilyName, customUser.FirstName), new Claim(JwtClaimTypes.Email, customUser.Email) }; } |
Modification de la configuration d�IdentityServer
Nous devons maintenant modifier la configuration d�IdentityServer pour enregistrer notre impl�mentation de IProfileService et supprimer l�enregistrement du TestUserStore :
services.AddIdentityServer()
.AddDeveloperSigningCredential()
.AddInMemoryIdentityResources(Config.GetIdentityResources())
.AddInMemoryApiResources(Config.GetApiResources())
.AddProfileService<ProfileService>();
C�est tout. Vous pouvez tester votre application et vous authentifier en utilisant l�application MvcAppClient :
Impl�mentation de l�interface IResourceOwnerPasswordValidator
Su vous souhaitez que d�autres clients puissent obtenir des jetons d�authentification en fournissant directement leur nom d�utilisateur et leur mot de passe au token endpoint, vous devez fournir votre propre impl�mentation de l�interface IResourceOwnerPasswordValidator.
Supposons que nous voulons que l�application ConsoleAppClient utilise ce mode. Nous allons dans un premier temps changer son GrantTypes dans le Config.cs pour utiliser ResourceOwnerPassword :
Code c# : | S�lectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 | new Client { ClientId = "consoleappclient", AllowedGrantTypes = GrantTypes.ResourceOwnerPassword, ClientSecrets = { new Secret("secret".Sha256()) }, AllowedScopes = { "testapi" } }, |
Ensuite impl�menter l�interface IResourceOwnerPasswordValidator pour valider l�identit� de l�utilisateur en utilisant notre UserRepository :
Code c# : | S�lectionner tout |
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 | public class ResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator { private IUserRepository _userRepository { get; set; } public ResourceOwnerPasswordValidator(IUserRepository userRepository) { _userRepository = userRepository; } public Task ValidateAsync(ResourceOwnerPasswordValidationContext context) { var customUser = _userRepository.FindByUserName(context.UserName); if (customUser != null && customUser.Password.Equals(context.Password)) { context.Result = new GrantValidationResult( subject: customUser.SubjectId, authenticationMethod: OidcConstants.AuthenticationMethods.Password); } else { context.Result = new GrantValidationResult( TokenRequestErrors.InvalidGrant, "invalid credential"); } return Task.FromResult(0); } } |
Modifier la configuration de IdentityServer pour utiliser notre impl�mentation de cette interface :
Code c# : | S�lectionner tout |
1 2 3 4 5 6 | services.AddIdentityServer() .AddDeveloperSigningCredential() .AddInMemoryIdentityResources(Config.GetIdentityResources()) .AddInMemoryApiResources(Config.GetApiResources()) .AddResourceOwnerValidator<ResourceOwnerPasswordValidator>() .AddProfileService<ProfileService>(); |
Maintenant, il ne nous reste plus qu�� mettre � jour le client pour passer ses informations d�identification lors de l�appel d�une ressource s�curis�e :
Code c# : | S�lectionner tout |
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 31 32 33 34 35 36 37 | static async Task CallWebApiR() { // discover endpoints from metadata var disco = await DiscoveryClient.GetAsync("https://localhost:5001"); if (disco.IsError) { Console.WriteLine(disco.Error); return; } // request token var tokenClient = new TokenClient(disco.TokenEndpoint, "consoleappclient", "secret"); var tokenResponse = await tokenClient.RequestResourceOwnerPasswordAsync("alice", "alice", "testapi"); if (tokenResponse.IsError) { Console.WriteLine(tokenResponse.Error); return; } Console.WriteLine(tokenResponse.Json); // call api var client = new HttpClient(); client.SetBearerToken(tokenResponse.AccessToken); var response = await client.GetAsync("https://localhost:5003/api/secure"); if (!response.IsSuccessStatusCode) { Console.WriteLine(response.StatusCode); } else { var content = await response.Content.ReadAsStringAsync(); Console.WriteLine(JArray.Parse(content)); } } |
� l�ex�cution, on obtient ce qui suit :
Maintenant, vous �tes capables d'int�grer vos services d'acc�s aux donn�es d'authentification avec IdentityServer, fournir votre propre impl�mentation de certaines interfaces pour partager des informations dans les revendications et fournir votre propre m�canisme de validation de mot de passe.
R�f�rences :
- http://docs.identityserver.io/en/release/index.html
- https://github.com/IdentityServer/Id....Quickstart.UI
- https://github.com/IdentityServer/IdentityServer4
Vous avez lu gratuitement 0 articles depuis plus d'un an.
Soutenez le club developpez.com en souscrivant un abonnement pour que nous puissions continuer � vous proposer des publications.
Soutenez le club developpez.com en souscrivant un abonnement pour que nous puissions continuer � vous proposer des publications.